BT_EXPIRE: tester builds stop working 14 days after build (compile gate)

Distributed test zips go stale and stray.  BT_EXPIRE (CMake option,
DEFAULT ON so a zip can never accidentally ship without it) makes the
exe refuse to start BT_EXPIRE_DAYS (default 14) after its build day:
boot logs the remaining window, an expired build logs + shows a
"test build expired -- ask the operator for the current build" box and
exits.  btversion.h now stamps BT_BUILD_UNIX (UTC-midnight-rounded so
the only-on-change header churns once per day, not every build) +
BT_BUILD_DATE.  Dev is unaffected: every build re-stamps the day.

-DBT_EXPIRE=OFF builds a non-expiring exe; mkdist reads the cache and
marks such a zip "-noexpire" + prints a warning, and the tester README
expiry note is {EXPIRE}-marker-gated like the steam lines.  Second
compile-gate exception after BT_STEAM (convention updated in
glass-cockpit.md).  Quality gate, not DRM -- a clock rollback defeats it.

Verified live: default build logs "test build window: 14 day(s) left";
a -DBT_EXPIRE_DAYS=0 build logs TEST BUILD EXPIRED and parks on the
explanation box (window title checked) without starting the game;
restored to 14 and rebuilt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-22 13:39:19 -05:00
co-authored by Claude Opus 4.8
parent 753540de96
commit eb618fe9e6
6 changed files with 85 additions and 3 deletions
+16
View File
@@ -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
+10 -3
View File
@@ -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.
+34
View File
@@ -24,6 +24,7 @@
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <ctime> // BT_EXPIRE tripwire (time)
#include <string>
#include <vector>
@@ -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).
+2
View File
@@ -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.
+9
View File
@@ -7,6 +7,13 @@
#
# Invoked: cmake -DOUT=<header> -DSRC=<repo root> -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
+14
View File
@@ -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