diff --git a/CMakeLists.txt b/CMakeLists.txt index eaded18..6ea3300 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,19 @@ set(DXSDK "C:/Program Files (x86)/Microsoft DirectX SDK (June 2010)" CACHE PATH option(BT_STEAM "Steam transport + lobby (Steamworks SDK -- EXCLUDE from public releases)" OFF) message(STATUS "bt411: BT_STEAM=${BT_STEAM}") +#===========================================================================# +# Tester-build expiry tripwire (2026-07-22). Distributed test zips go stale +# and stray: with BT_EXPIRE on (the DEFAULT, so a zip can never accidentally +# ship without it) the exe refuses to start BT_EXPIRE_DAYS after its build +# date, telling the tester to fetch a current build. Dev is unaffected -- +# btversion.h re-stamps the build day on every build. Disable for a +# non-expiring build with -DBT_EXPIRE=OFF (mkdist marks such a zip +# '-noexpire'). Quality gate, not DRM -- a clock rollback defeats it. +#===========================================================================# +option(BT_EXPIRE "Expire the exe BT_EXPIRE_DAYS after its build date" ON) +set(BT_EXPIRE_DAYS 14 CACHE STRING "Days a BT_EXPIRE build keeps running") +message(STATUS "bt411: BT_EXPIRE=${BT_EXPIRE} (${BT_EXPIRE_DAYS} days)") + set(BT_DEFS WIN32 _WINDOWS UNICODE _UNICODE NOMINMAX _CRT_SECURE_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS @@ -366,6 +379,9 @@ target_sources(btl4 PRIVATE ) target_include_directories(btl4 BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/game/glass") target_compile_definitions(btl4 PRIVATE ${BT_DEFS}) +if(BT_EXPIRE) + target_compile_definitions(btl4 PRIVATE BT_EXPIRE=1 BT_EXPIRE_DAYS=${BT_EXPIRE_DAYS}) +endif() target_compile_options(btl4 PRIVATE ${BT_OPTS}) target_link_libraries(btl4 PRIVATE bt410_l4 diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 33a02e8..67fdf83 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -49,9 +49,16 @@ version skew (a stale steam exe shipped 2 days behind), tripled build time, and glass + scripted click → ConfigureMappables end-to-end; exe runs WITHOUT steam_api.dll present; glass zero-arg → menu; 2-node loopback MP (145 ticks each); `BT_STEAM_NET=1` without Steam → "staying on Winsock" + game runs. -- **Convention:** every `BT_*` name is a RUNTIME env gate (`BTEnvOn`) — with the single - exception of `BT_STEAM`, the license compile gate above. Do not add another compile gate - without updating this file. +- **Convention:** every `BT_*` name is a RUNTIME env gate (`BTEnvOn`) — with TWO exceptions, + both compile gates: `BT_STEAM` (the license gate above) and **`BT_EXPIRE` (2026-07-22, the + tester-build expiry tripwire)** — default ON, the exe refuses to start `BT_EXPIRE_DAYS` + (default 14) after its build DAY (`btversion.h BT_BUILD_UNIX`, UTC-midnight-rounded so the + only-on-change header churns once per day, not per build). Boot logs `[boot] test build + window: N day(s) left`; expired = log + MessageBox + exit. Disable: `-DBT_EXPIRE=OFF` + (mkdist marks such a zip `-noexpire` + warns; README expiry note is `{EXPIRE}`-marker-gated). + Quality gate, not DRM — a clock rollback defeats it. Verified both paths live + (`-DBT_EXPIRE_DAYS=0` force-expire run). Do not add another compile gate without updating + this file. - No weak-linkage/optional symbols — `/FORCE` turns unresolved externals into runtime AVs ([[reconstruction-gotchas]]); everything links always, so this class of trap no longer varies by build flavor. diff --git a/game/btl4main.cpp b/game/btl4main.cpp index 34a6750..4798b03 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -24,6 +24,7 @@ #include #include #include +#include // BT_EXPIRE tripwire (time) #include #include @@ -542,6 +543,39 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine std::cout << "BattleTech " << BT_VERSION_FULL << " (win32 reconstruction of Tesla 4.10)" << std::endl << std::flush; +#ifdef BT_EXPIRE + // TESTER-BUILD EXPIRY TRIPWIRE (compile gate BT_EXPIRE, default ON -- + // see CMakeLists). Distributed zips go stale and stray; refuse to run + // BT_EXPIRE_DAYS after the build day (btversion.h BT_BUILD_UNIX, UTC + // midnight). Dev builds re-stamp every build, so this never fires + // locally. Quality gate, not DRM. + { + const time_t expires = (time_t)BT_BUILD_UNIX + + (time_t)BT_EXPIRE_DAYS * 86400; + const time_t now = time(0); + if (now >= expires) + { + std::cout << "[boot] TEST BUILD EXPIRED (built " << BT_BUILD_DATE + << ", " << BT_EXPIRE_DAYS << "-day test window)" + << std::endl << std::flush; + char expired_text[256]; + sprintf(expired_text, + "This BattleTech test build (%s, built %s) has expired.\n\n" + "Test builds stop working %d days after they are made so\n" + "stale copies don't wander around.\n\n" + "Ask the operator for the current build.", + BT_VERSION_STRING, BT_BUILD_DATE, (int)BT_EXPIRE_DAYS); + MessageBoxA(NULL, expired_text, + "BattleTech -- test build expired", MB_OK | MB_ICONWARNING); + return 0; + } + int days_left = (int)((expires - now + 86399) / 86400); + std::cout << "[boot] test build window: " << days_left + << " day(s) left (built " << BT_BUILD_DATE << ")" + << std::endl << std::flush; + } +#endif + // CPU pin (timing stability). BT_AFFINITY overrides the mask; =0 disables -- // required for multi-instance runs (two instances pinned to core 0 starve // each other, and the L4NET connect-retry loop busy-waits). diff --git a/players/README.txt b/players/README.txt index 73f6ad0..0fa6c6a 100644 --- a/players/README.txt +++ b/players/README.txt @@ -43,3 +43,5 @@ An Xbox controller works out of the box. Rebind keys in content\bindings.txt (regenerates with defaults if deleted). This build: {VERSION}. Private -- do not redistribute. +{EXPIRE}Test builds STOP WORKING 14 days after they are made -- if the game +{EXPIRE}says it has expired, just grab the operator's newest zip. diff --git a/tools/btversion.cmake b/tools/btversion.cmake index 3e37a5c..cfa06dd 100644 --- a/tools/btversion.cmake +++ b/tools/btversion.cmake @@ -7,6 +7,13 @@ # # Invoked: cmake -DOUT=
-DSRC= -P btversion.cmake +# Build DAY stamp (UTC midnight epoch) for the tester-build expiry tripwire +# (BT_EXPIRE). Day precision on purpose: the header is written only-on-change, +# so a per-second stamp would churn btl4main.obj on every no-op build. +string(TIMESTAMP BT_NOW_UNIX "%s" UTC) +math(EXPR BT_BUILD_UNIX "(${BT_NOW_UNIX} / 86400) * 86400") +string(TIMESTAMP BT_BUILD_DATE "%Y-%m-%d" UTC) + execute_process(COMMAND git rev-list --count HEAD WORKING_DIRECTORY "${SRC}" OUTPUT_VARIABLE BT_BUILD OUTPUT_STRIP_TRAILING_WHITESPACE @@ -44,6 +51,8 @@ set(CONTENT "// generated by tools/btversion.cmake -- do not edit, not in git #define BT_VERSION_STRING \"4.11.${BT_BUILD}\" #define BT_VERSION_HASH \"${BT_HASH}${BT_DIRTY}\" #define BT_VERSION_FULL \"4.11.${BT_BUILD} (${BT_HASH}${BT_DIRTY})\" +#define BT_BUILD_UNIX ${BT_BUILD_UNIX} +#define BT_BUILD_DATE \"${BT_BUILD_DATE}\" ") # write only when changed so incremental builds don't churn diff --git a/tools/mkdist.py b/tools/mkdist.py index 3c4e6a9..25cfdea 100644 --- a/tools/mkdist.py +++ b/tools/mkdist.py @@ -54,6 +54,7 @@ def main(): # the CMake cache; a steam-OFF zip must carry neither play_steam.bat nor # steam_api.dll (a stale DLL can linger from an earlier ON build). steam_on = False + cache = "" try: cache = open("build/CMakeCache.txt", encoding="utf-8", errors="ignore").read() steam_on = "BT_STEAM:BOOL=ON" in cache @@ -62,6 +63,15 @@ def main(): print(" BT_STEAM=%s (from build/CMakeCache.txt)" % ("ON" if steam_on else "OFF")) if not steam_on: name += "-nosteam" # distinct name: never clobbers the dev-flavor zip + + # TESTER-BUILD EXPIRY (BT_EXPIRE, default ON): a zip cut from an + # expire-OFF build never dies in the field -- mark it loudly so it is a + # deliberate act, not an accident. + expire_on = "BT_EXPIRE:BOOL=ON" in cache if cache else True + print(" BT_EXPIRE=%s (from build/CMakeCache.txt)" % ("ON" if expire_on else "OFF")) + if not expire_on: + name += "-noexpire" + print(" WARNING: this zip NEVER EXPIRES -- intended for distribution?") zpath = os.path.join(outdir, name + ".zip") exes = ["build/Release/btl4.exe"] @@ -102,6 +112,10 @@ def main(): lines = [l.replace("{STEAM}", "") for l in lines] else: # steam-free zip: strip them lines = [l for l in lines if "{STEAM}" not in l] + if expire_on: # same treatment for expiry note + lines = [l.replace("{EXPIRE}", "") for l in lines] + else: + lines = [l for l in lines if "{EXPIRE}" not in l] readme = "".join(lines) z.writestr(name + "/README.txt", readme) for p in bats: # bats at the zip ROOT (next to