Search Results for

    Show / Hide Table of Contents

    Matching Clients and Servers

    [!SUMMARY] This page explains:

    • Why a client and a server from different builds break each other instead of refusing to connect
    • How builds are stamped, grouped and checked so that matching pairs are easy to find
    • What to do when a mismatched session breaks
    • Why builds from the same commit still disagreed in October 2026, and the rule that follows from it

    A client and a server that were built from different commits do not refuse each other on their own. They connect, and then misread each other's messages: FishNet numbers RPCs by their position within a NetworkBehaviour rather than by name, so one added or removed RPC renumbers everything below it and the receiving side deserialises a payload with the wrong method's parameter list. The 1.0.4 testing pair failed exactly this way — the server read a player name where it expected a lobby Guid, logged Size of -50 is invalid, and kicked the client with unhandled PacketId.

    Three things now keep that from happening. A fourth trap, where even builds from the same commit disagree, is described at the end of this page.


    Every player knows which commit it is

    BuildCommand stamps the commit into the player (Assets/Resources/build-stamp.json, generated per build and deleted afterwards) and writes build-info.json next to the executable, inside the .zip, the .tar.gz and the server Docker image. The .apk cannot carry a loose file, so its build-info.json is placed beside it in Dist/.

    Every Player.log now opens with the build it came from:

    [BuildStamp] Windows 1.0.4, commit a1b2c3d, built 2026-09-21T16:30:00Z, Unity 6000.3.8f1
    

    Downloads are grouped per commit

    Testing pipelines upload to Testing/<branch>/<commit>/, pre-releases to Pre-Release/<X.Y.Z-pre.N>/, never to a shared name. A latest.json next to those folders names the newest folder for which every platform job succeeded and reported the same commit — take the client and the server from the folder it names and they are guaranteed to belong together. Releases go to Build Archive/<X.Y.Z>/. Only once every platform has passed that check does the publish stage copy them into Public/, all at once, where they keep the same names from release to release — that is what the website's static download links point at.

    The server image works the same way: build-server pushes itlfi/anywhere-academy-server:staging-<commit>-<pipeline>, and the deployable tags (:pre-release, or :X.Y.Z, :latest and :stable) are moved onto it only after the same check. The staging tag is deleted again once it has been promoted. How those tags reach the hosted servers is described in Server Images.

    The server warns about a mismatched client

    BuildStampAuthenticator exchanges commits before a connection is authenticated. A mismatch is logged as a warning on each side, naming both builds, and the connection goes through:

    [BuildStamp] Build mismatch: client '0' (Windows 1.0.4 @ a1b2c3d...) was built from commit
    'a1b2c3d...', this server from 'e4f5a6b...'. Accepting it: ...
    

    It is not rejected, because a client-only change (UI, settings, art) is expected to work against the previous server, and a commit comparison cannot tell such a change apart from one that touched networked code. When the session then breaks with reader errors or "unhandled PacketId" kicks, this warning is the first line to look for.

    A player with no stamp (a build made outside the build pipeline, or from a checkout git could not read) and a client from before this check, which never reports its build, are let through with a warning as well. The latter only after a 15-second wait for the report it never sends.


    If a mismatched session breaks

    1. Read the commit from the client's Player.log and from the server's log.
    2. Download both from the same Testing/<branch>/<commit>/ folder: the client, and the Anywhere_Academy_Server.tar.gz next to it. (There is no per-commit Docker tag to deploy; the :staging-<commit>-<pipeline> tag is deleted once it has been promoted.)
    3. Re-run the whole pipeline rather than a single job: a retried job builds from the current branch head, which may no longer be the commit the other platforms were built from. The publish stage stops a pipeline whose platforms disagree, so this shows up as a failed job rather than as a broken download.

    Incident: same commit, different build OS (October 2026)

    A matching commit is not always enough. In October 2026 a Windows client and a Linux server, both built by CI from the same commit, failed exactly like the 1.0.4 pair. The Quest APK, also built on Linux, worked with the same server.

    Cause. Three behaviours combined:

    1. Unity sorts the source files of an assembly before compiling them, and the sort depends on the OS the editor runs on (NiceIO.NPath): case-sensitive on Linux, case-insensitive on Windows and macOS. GlobalPlayerController.Server.cs therefore comes before GlobalPlayerController.cs on Linux (S < c) and after it on Windows.
    2. The compiler emits the members of a partial class in source file order.
    3. FishNet numbers RPCs and SyncTypes by member position (see above), not by name.

    AnyVR's GlobalPlayerController declares its RPCs across two partial files, so its RPC ids differed between Windows and Linux builds. A Windows client's ServerRPC_SetPlatformType (id 4) reached the Linux server as ServerRPC_JoinLobby, which read the following packet as a lobby Guid — the "lobby id" in the server log contained bytes of the player name.

    The commit check above could not catch this: both builds carried the same commit. It was found by reading the RPC ids FishNet wove into AnyVR.LobbySystem.dll of both builds (with Mono.Cecil) and comparing them.

    Resolution. AnyVR's next update stops splitting the RPCs of a NetworkBehaviour across partial files. A FishNet patch that numbers members by name instead was tested and works across OSes (commit e0f52b74, Packages/com.firstgeargames.fishnet/PATCHES.md), but was not adopted.

    Prevention. Every build job now records the network ids FishNet wove into its player (Tools/Build/network-table.ps1 → Build/Artifacts/<target>/network-table.txt, and its SHA-256 as networkTableSha256 in build-info.json). The publish stage compares them across platforms (Tools/Build/compare-network-tables.ps1) and fails before anything is published if one networked type has different ids on different platforms, naming the type and both id lists.

    Warning

    Declare all RPCs and SyncTypes (SyncVar, SyncList, …) of a NetworkBehaviour in one file. Splitting them across partial files makes their network ids depend on the OS that builds the player, and the CI builds the Windows client on Windows and everything else on Linux.

    In This Article
    Back to top Anywhere Academy Documentation