Decode replay orders against the replay's format version - #177
Open
kylelutze wants to merge 2 commits into
Open
Conversation
NetSendOrder::decodeData hardcoded VERSION_MINOR when handing the payload to Order::getOrder. That is correct for live network traffic, where both peers run this build, but wrong for a replay: a replay is a stored format that may predate the running build, and ReplayReader accepts anything from REPLAY_MINIMUM_VERSION_MINOR up. Nothing misparses today. OrderCreate's flagRadius gate at version 78 is the only version gate anywhere in the order layer, and it sits below the replay floor of 86, so every order in every accepted replay decodes identically at 86, 87 and 88. The next order-format gate would not be so lucky -- it would silently misread every replay written before it, with no error and no checksum to catch it. Carry the version on NetSendOrder instead of changing the decodeData signature, which is a NetMessage virtual with 62 overrides. It defaults to VERSION_MINOR, so the network path and the writer are unaffected; only ReplayReader overrides it, from the version in the replay's own header. ReplayStepCounterTest gains a case asserting the version actually reaches Order::getOrder. Since no order parses differently across the supported replay range, the plumbing has to be observed directly rather than inferred from a decoded order; the test's stubbed getOrder records the version it was called with. Verified to fail before this change. G2 replay verification is byte-equal against the cpp-refactor baseline.
Giszmo
approved these changes
Sep 7, 2026
Giszmo
left a comment
Contributor
There was a problem hiding this comment.
Reviewed on head 943727e. Approve, no blocking findings.
What it does. NetSendOrder gets a decodeVersionMinor member defaulting to VERSION_MINOR; only ReplayReader overrides it from the replay header, at both decode sites (initial scan and retrieveOrder). Network path, ReplayWriter and the other NetMessage subclasses are untouched. Minimal and the right shape given decodeData is a virtual with a fixed signature.
Claims verified:
- The only version gate in the order layer is
FILE_FORMAT_VERSION_ORDER_CREATE_FLAG_RADIUS(78) inOrderBuilding.cpp:49-60; grepped everyversionMinorcomparison acrossOrder*.cpp. 78 is below the replay floor of 86, so this is behaviour-neutral today and only matters for the next format bump. - Built
test/with scons:ReplayStepCounterTestALL PASS,NetSendOrderDecodeTest8/8. - Not vacuous: reverting the one-line fix in
OrderMessages.cppand rebuilding givesFAILURES: 2(both newdecodeVersionchecks). - CI runs every
./*Testbinary intest/("Build and run the tests" step), so the regression test is live. - The
ReplayReader::setCheckSumside note is correct: no caller anywhere insrc, only the writer side is fed fromEngineRun.cpp:121, so the checksum-mismatch guard inretrieveOrderis dead code. Worth a follow-up issue, not this PR.
Nits, non-blocking:
ReplayReadernow holds bothversionMinorandwideStepCounterderived from the same header field. Deriving the flag from the member would remove one piece of duplicated state; mirroring the existing pattern is fine too.- Member init in the ctor body rather than the initializer list matches the surrounding code.
Review by Bob (Fable 5.1 agent) at Leo's request.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NetSendOrder::decodeDatahardcodedVERSION_MINORwhen handing the payload toOrder::getOrder:order = Order::getOrder(buffer.data(), size, VERSION_MINOR);That is right for live network traffic — both peers run this build, so the current version is by definition the correct one. It is wrong for a replay. A replay is a stored format that may predate the running build, and
ReplayReaderaccepts anything fromREPLAY_MINIMUM_VERSION_MINOR(86) up toVERSION_MINOR(88). The samedecodeDataserves both paths.Nothing misparses today
FILE_FORMAT_VERSION_ORDER_CREATE_FLAG_RADIUS(78) is the only version gate anywhere in the order layer — every other order'ssetDatatakesversionMinorand ignores it. 78 sits below the replay floor of 86, so every order in every accepted replay decodes identically at 86, 87 and 88.The next order-format gate would not be so lucky. It would silently misread every replay written before it — wrong offset or wrong width, no error raised, and no checksum to catch it (see the note below).
Approach
decodeDatais aNetMessagevirtual with 62 overrides, so changing its signature is off the table. InsteadNetSendOrdercarries the version as a member defaulting toVERSION_MINOR. The network path, the writer, and the other 61 subclasses are untouched; onlyReplayReaderoverrides it, from the version in the replay's own header — mirroring how the adjacentwideStepCounterflag is already derived from that same value.Testing
ReplayStepCounterTestgains a case asserting the version actually reachesOrder::getOrder. Because no order parses differently across the supported replay range, there is no behavioural delta to observe — the plumbing has to be checked directly, so the test's stubbedgetOrderrecords the version it was called with.ReplayStepCounterTest: 22/22 PASS.FAILURES: 1), then pass again once restored — it is not vacuous../build/src/glob2 --nox games/G2.game 90000 1is byte-equal totests/baselines/cpp-refactor.replay. No determinism impact; no baseline change.Playback could not be exercised end-to-end:
-replayis unreachable headlessly, sinceGlob2::runreturns fromrunNoX()before the replay branch andrunNoX()only supportsinitCustom.Unrelated pre-existing issue noticed
ReplayReader::setCheckSumis never called by the engine — only theReplayWritercounterpart is, atEngineRun.cpp:121. SoReplayReader::checksumstays 0 and the "checksums don't match!" guard inretrieveOrderis dead code: a replay that decoded wrong would play on silently. Not touched here.