Releases of the official OpenVersus installer MVS Buddy are available in the OpenVersus MVS Buddy repository. If you just want to install OpenVersus, please download and run MVS Buddy, as it is by far the easiest and most user-friendly method to install and manage OpenVersus installations. This repository (ovs-client) is aimed toward developers and its releases are primarily consumed by MVS Buddy and the client's built-in updater.
To get MVS Buddy for Windows, download the zip file here, and then run then exe inside of it.
To get MVS Buddy for Steam Deck and ordinary Linux installs. download the tarball here, and then run the the MvsBuddy file inside of it. It should extract from the tarball with the exec bit already set.
This is the source repository of the OpenVersus client-side DLL, a fork based on the publicly-available original work of thethiny and multiversuskoth.
The source code for the OpenVersus client-side DLL is available here, as well as prebuilt binary releases of the client. Binary releases are also maintained in this repo for backward-compatibility.
The current release is built with Visual Studio 2022 and 2026, using toolchain version 145. Opening the solution and building is all that's necessary at this point to locally build a version of the client. A more comprehensive README will be committed in the near future.
- Anti-Pak Signature Patch. Allows you to run
.pakfiles with invalid.sigsignatures (custom local skins replacing an existing skin or emote, etc). - Anti-uToc Signature Patch. Allows you to use
uTocfiles (and.uCas) with invalid Signature Headers (see above). - Custom Server addresses: Allows you to change the server endpoint (whether it points to official OpenVersus servers or not. OpenVersus server code is also open source and available here)
- Bypasses the game's built-in network sunset date/killswitch checks (necessary for online play)
- Supports using custom in-game notification dialogs
- Restores the original post-match behavior: after the final stock, everyone still in the match will again see the winner(s) postgame character inputs, instead of the winner standing dead still until the postmatch screen appears.
- Auto-update: at launch, checks for a newer version and installs it, then closes the game so the next launch runs the update (
AutoUpdate, on by default) - P2P Rollback node: runs the OpenVersus rollback netcode server beside the game and closes it with the game; this is for matches that run directly between the players' machines via P2P instead of on a central server (
RollbackNode, on by default; see Rollback node) - Game files: at launch, downloads the paks (OpenVersus's own game files) the current release needs, each checked by thier known-good and trusted SHA-256 hash; they are required, so this happens even with
AutoUpdateoff (see Paks) - Works with the Steam and the Epic Games Store versions of the game, and should also work with other versions sourced from neither (untested)
- Neuters the game's built-in Analytics telemetry (and holy shit there was a lot of it)
These only apply in the following modes for now: Free For All, the experimental Testing Grounds queue, and Custom Lobbies that turn them on. The regular 1v1 and 2v2 queues play as they always have.
- Free For All and 2v2 Individual Stocks rules: each fighter has their own lives and stays out once all their stocks have been taken; Free For All ends when only one fighter remains.
- Team Attack/Friendly Fire: (most) attacks will damage your partner. Abilities/attacks that are clearly 2v2/team-focused do not.
- Beta Speed: matches run at the same speed as Beta MVS.
Comprehensive install instructions are available in the readme.txt file included in the release zip file. An installer will be provided with a later build, but for right now, it's just a simple drag & drop.
Settings live in OpenVersus.toml beside the plugin, which the mod creates with default values when it's missing. sample.toml in this repo is a copy of that default file, for reference. Text values go in double quotes (ServerUrl = "https://prod.openversus.org/"); true, false and numbers don't. Lines starting with # are comments, and a # after a value starts a comment too. If the file has a mistake, the mod runs on default settings and leaves the file alone, and a message in the game (and logs/OpenVersus.log) says which line and column to fix. If you still have an OpenVersus.ini from an older version, the first launch converts it to OpenVersus.toml, keeping your settings and comments, and deletes the ini. If an OpenVersus.toml is already there (say, after extracting a new release over an old install), the ini's settings are merged into it wherever it still has the default value, and the ini is deleted. Versions before the 2026.04.08.14 prerelease won't start without a settings file; since then, a missing one is created with default values.
The OpenVersus mod client is a C#/.NET 10 NativeAOT MultiVersus mod. Its native build environment
is a Linux machine using xwin and lld to produce a native
Windows PE binary, but it should have no issue building on a Windows host. It is a
reimplementation of the former C++ client: an .asi loaded by Ultimate ASI Loader. Its files are TOML:
a player's OpenVersus.ini becomes OpenVersus.toml on the first launch, OVSState.ini becomes
OVSState.toml, and PatternsCache.cache is replaced by PatternsCache.toml (see
Settings).
Status. Verified in the game on Windows, Linux (Proton) and Steam Deck, including
matches with netstats on and the post-match freeze patch. Also verified in the game: the C++
client updating itself to this one (from a legacy plain-http ServerUrl), which then moved into
plugins/OpenVersus/ and converted the ini; and the updater refusing an offer whose built-in
version was not newer. Never tested on macOS.
By default, matches run directly between the players' own machines (P2P rollback), and only fail back to a central rollback server in the event of connection failures. The game is told to
connect to 127.0.0.1 and a port, where a version of the OpenVersus rollback netcode server (OVS.Rollback.Node, from the
ovs-rollback-server repository) must be listening. The mod runs that node:
- At startup, when
RollbackNodeis on and the game's server connection points at an OpenVersus server, it startsnode/win-x64/OVS.Rollback.Node.exefrom the mod's folder (plugins/OpenVersus/). Under Proton or Wine it startsnode/linux-x64/OVS.Rollback.Nodeinstead, through Wine'sstart /unix, so the node runs natively on Linux and the Steam Deck; the Windows build runs under Wine only when the Linux one is missing. On a Mac (CrossOver) it startsnode/OVS.Rollback.Node.shthe same way, a launcher that runsnode/osx-arm64/OVS.Rollback.Nodeon Apple silicon ornode/osx-x64/OVS.Rollback.Nodeon Intel (the Mac's architecture is not visible from inside Wine), and the Linux node anywhere else. - The node is told to take any free UDP port and writes it to
node/port-<game pid>.txt; the mod reports that port to the server with the player's identity (nodePortin/api/identify), and the server names it in the match-found notification. A node that cannot be started leaves the port at 0 and the server uses its default. - Everything the node needs travels on its command line (
--port-file,--parent-token,--parent-timeout,--server,--rendezvous), because Wine does not pass the Windows environment to a Linux program. [Server.Game] P2PRegistryandP2PRegistryPortname the rendezvous service the nodes find each other through (defaultsp2p.openversus.organd 41235). The host is a DNS name the server side points wherever the service runs, so moving it needs no client release; the port has its own row for the same reason, and a value that is not a port falls back to 41235 with a warning. An empty host sends every P2P match through a relay. The node resolves the name once at startup, so a change takes effect at the next launch of the game.- The mod sends the node a keepalive every second (its P2P protocol's Parent message, with a token
only this launch knows). On Windows the node is also in a kill-on-close job, so a game crash ends it
at once; through
start /unixit is not the mod's child, and the keepalive stopping is what ends it, within ten seconds. A normal exit stops it directly.
The node ships with every release and the updater keeps it current, like the paks: the release
carries one archive per platform (OVS_Node_<version>_<platform>.zip or .tar.gz), the mod tells
the server which node version it runs beside the game (read from the node's executable, where the
build stamps it; a node without one, or none at all, counts as out of date), and when the server
says the release's is different, Net/NodeUpdate.cs downloads the archive for the platform the
game runs on (a Mac is asked which it is through a small script, since that is not visible from
inside Wine), checks its size and SHA-256, unpacks it and swaps it into node/<platform>/, keeping
the previous build as node/<platform>.old for one launch. Under Proton, Wine or CrossOver the
unpacked node and the launcher get their execute bits through Wine's start /unix and chmod,
since files written through Wine have none. This happens at launch before the node is started, so
no relaunch is needed, and whatever AutoUpdate says, since peers' nodes must match; a failure
leaves the previous node in place and the game plays on (matches then go through a relay).
OpenVersus/: the.asiitself: theInitializeASIexport the loader calls, andOpenVersusInfo(Plugin.cs).OpenVersus.Core/: everything else, so the tests and the Wine harness can share it.Client.cs: startup: other copies retired, the move intoplugins/OpenVersus/, then the C++ client's order: settings, console, hooks, background work.DuplicatePlugins.cs,Layout.cs,LoaderConfig.cs: other copies of the plugin, where the mod lives, and what Ultimate ASI Loader will load (see Where it lives).Config/: the TOML files (TomlConfig:Settings,State,PatternCache), the commented default settings file (DefaultConfig), what every config file shares (ConfigFile), the conversion from the old settings ini (SettingsMigration), and the reader for the old ini files (IniFile).Game/: the game's structures and offsets (Mvs,UE), the function registry (GameFunctions), the object array and finder (Objects), reflection, dialogs and toasts (GameUi), the game-thread queue (GameThread) and the engine-ready gate (Engine).Hooks/: one class per patch or hook;Hooking/HookGuardkeeps exceptions out of the game.Memory/: byte patterns, call-site redirects, trampolines, code writes, guarded reads.Net/: HTTP, the notification poller, auto-update, JSON.P2P/: the rollback node the mod runs beside the game (RollbackNode): how it is started on Windows and under Proton, the file it reports its port in, and the keepalive that lets it outlive a crashed game by seconds rather than forever.Identity/: the Steam/Epic identity, hardware fingerprint and per-device identifiers sent to the server.NetStats/: the per-match rollback statistics log.Native/: P/Invoke declarations, with Windows SDK names.
OpenVersus.HookTest/: a plugin for the Wine harness that proves the hooking layer on a small host, with no game involved.OpenVersus.Tests/: xunit tests that run on the Linux host, including the object finder against a synthetic game (FakeGame.cs).wine-host/: the harness.run.shcross-compileshost.c, publishes the test plugin and runs both under Wine in a private prefix.
Requires the .NET 10 SDK. Two scripts wrap the commands below and check the prerequisites:
build.sh # Linux (macOS untested): build, test, publish OpenVersus_<version>.asi; --help for options
build.ps1 # Windows (PowerShell; build.cmd runs it from cmd or a double-click)build.sh |
build.ps1 |
|
|---|---|---|
| (no command) | (no command) | build, run the tests, publish |
test |
test |
build and run the tests only; needs no Windows toolchain |
publish |
publish |
publish the plugin only |
harness |
run the Wine harness | |
clean |
clean |
remove every bin/ and obj/ directory |
--install DIR |
-Install DIR |
copy the plugin into DIR/OpenVersus (DIR is the game's plugins folder), renaming any OpenVersus*.asi in either to .bak |
--rwx |
-Rwx |
read-write-execute trampolines (see below) |
--skip-tests |
-SkipTests |
skip the tests in the default command |
--accept-license |
accept the Visual Studio Build Tools license for the Windows SDK sysroot |
On Windows the publish needs the "Desktop development with C++" workload of Visual Studio or its
Build Tools. On Linux it needs lld-link (package lld) and
xwin on PATH. The first publish downloads the Windows
SDK sysroot (about 2.4 GB) into ~/.cache/xwin, which needs its license accepted: use
--accept-license, ACCEPT_VS_BUILD_TOOLS_LICENSE=true, or answer yes when the script asks. To
reuse a sysroot you already have (xwin's own default is .xwin-cache), set XWinCache to that
directory, with a trailing slash.
By hand:
dotnet build OpenVersus.slnx # everything, for the host (tests, no AOT)
dotnet test OpenVersus.slnx
dotnet publish OpenVersus/OpenVersus.csproj -c Release -r win-x64 -p:AcceptVSBuildToolsLicense=true
wine-host/run.sh # the hooking layer, end to end under WineThe published plugin is OpenVersus/bin/Release/net10.0/win-x64/publish/OpenVersus_<version>.asi,
named from the VERSION file. Ultimate ASI Loader
loads every .asi in the game's folder and, by default, every .asi under scripts/ and
plugins/, subfolders included, so two versions side by side would both load; the plugin retires
extra copies itself (see Other copies of the plugin). The plugin
imports only system DLLs (KERNEL32, ADVAPI32, bcrypt, ole32) and the UCRT api-sets.
Trampoline pages are read-execute except while a stub is being written. -p:RwxTrampolines=true
on the publish (the scripts' --rwx) keeps them read-write-execute for their whole life, as the
C++ client did.
Release builds also write full XML documentation (OpenVersus.Core.xml, OpenVersus.xml). A public
member without a doc comment is a compile-time warning (CS1591), but not a build error.
.github/workflows/build.yml runs on every branch push and pull request, and can be started by
hand. It runs ./build.sh test and dotnet format --verify-no-changes, then publishes the plugin
on a GitHub-hosted Ubuntu runner with the same toolchain as above (lld from apt, xwin pinned by
version and checksum, the SDK sysroot cached between runs). Its artifact is the install exactly
as it ships:
xinput1_3.dll Ultimate ASI Loader, pinned by digest
LICENSE_Ultimate_ASI_Loader.txt
readme.txt from release-readme.txt
plugins/OpenVersus/OpenVersus_<version>.asi
plugins/OpenVersus/sample_config.toml sample.toml under a note that it is a sample
plugins/OpenVersus/LICENSE.txt
plugins/OpenVersus/VERSION.txt
The zip ships no OpenVersus.toml: the plugin writes it on first launch (the same text as
sample.toml), so extracting a release over an install never replaces a player's settings.
The loader's xinput1_3 build is only published under upstream's moving x64-latest release, so
when upstream replaces it the build fails with a message naming LOADER_SHA256: check the new
loader, then update the digest (and LOADER_LICENSE_URL's tag) in build.yml.
.github/workflows/release.yml publishes a release when a tag is pushed. It builds nothing: it
waits for the successful build run of the tagged commit and publishes that run's artifact, so
what ships is what was built and tested. To release:
- Bump
VERSIONand push. - Tag that commit with the bare version and push the tag:
git tag 2026.09.25.01 && git push origin 2026.09.25.01. There's no need to wait for the build; the release waits for it.
The tag must equal VERSION (a v prefix does not match). A suffix, as in 2026.09.25.01-test1,
publishes a prerelease; remove a test release with gh release delete <tag> --cleanup-tag. The
release holds OpenVersus_v<tag>.zip (the install, the rollback node for every platform inside),
OpenVersus_<version>.asi (the plugin alone, which is what the server's download_url points the
auto-updater at), OVS_Node_<version>_<platform>.zip / .tar.gz (the node alone, per platform,
which the updater installs; built in build.yml from the server repo at the commit NODE_REF
names and stamped with VERSION, so pin NODE_REF to a commit before tagging a release),
OpenVersus_<version>.zip (the package build.sh package makes: plugins/OpenVersus/ with the
plugin, its settings file and every node; what MVS Buddy installs), the OVS_* paks of the previous
release (GitHub's "latest", the newest that is not a prerelease), carried over so the new release
is complete from the moment it is published (new content replaces them by hand afterwards, as
before), a .sha256 beside each built file, and SHA256SUMS. The release is created as a draft and
published once every asset is on it. To attach node archives to a release by hand, the server
repo's OVSRollbackNode/tools/archive-nodes.sh <version> <dir> packs them from a
NODE_VERSION=<version> ./build.sh node build; the version in their names must be the release's.
The server offers a release's .asi asset and falls back to its zip when there is none; the
updater handles both, installing only the one .asi in a zip (or the one named for the offered
version) and nothing else from it. Before anything is installed, the download is checked against
the release's <download_url>.sha256: a mismatch, a missing .sha256, or one that cannot be
fetched or read, means no install this launch. A missing one counts as a mismatch because every
release the updater could install (only newer ones are) comes from this pipeline, which always
publishes them. Last, the plugin itself decides: the version built into it (its version resource,
read without loading it) must be newer than the running client and must be the version the server
offered, so a mislabeled offer can never make a client reinstall itself.
Some features need OpenVersus's own game files (paks: .pak, .utoc, .ucas and .sig). The
server's /ovs/client-version answer lists the release's files, its paks among them, and at launch,
before the engine opens its paks, Net/StartupUpdate.cs compares them with
%LOCALAPPDATA%\MultiVersus\Saved\Paks: one of the folders the engine mounts paks from, always
writable, and never touched by Steam's updates or file verification (under Proton, the prefix's
AppData). Net/PakUpdate.cs downloads whatever is missing or different into a staging folder,
checks every size and SHA-256, and installs them all or nothing, keeping the previous copy of each
replaced file; then the game closes, so the next launch mounts the new files. Paks download
whatever AutoUpdate says: the game crashes without the paks the server's content needs, so when
they cannot be brought up to date the game is closed rather than started. They download only from
the openversus account's GitHub releases ([Settings.Debug] ReleaseOwner, for testing a fork).
OVS paks installed by hand in the game's Content\Paks outrank Saved\Paks, so each is moved over
when it matches the release and moved out of the way when it does not. The working files live in
Saved\OpenVersus\: update-staging, pak-backup, old-content-paks (hand-installed paks moved
out of the way) and PakHashes.txt (local hashes by size and modified time, so unchanged files are
not hashed again). A release without paks leaves all of this with nothing to do.
| C++ | Here |
|---|---|
DllMain → OnInitializeHook |
Plugin.InitializeASI → Client.Initialize (NativeAOT cannot run managed code in DllMain; the loader calls the export right after LoadLibrary) |
PatternFinder, CachedPatternsMgr |
PatternResolver, PatternCache; the pattern parser has the C++ semantics (? is one byte) and a cached address is checked before use |
MakeProxyFromOpCode, InjectHook, Trampoline |
CallSite.Redirect, CallSite.Inject, Trampoline |
OVS::Hooks::* |
Hooks/*. The sig-check, sunset-date and post-match-freeze changes are byte patches (the sunset check runs thousands of times a minute); the endpoint and game-instance hooks are call redirects into [UnmanagedCallersOnly] methods guarded by HookGuard; the startup dialog and toast are a game-thread job |
MVSGame::* function globals |
GameFunctions: every resolved function by name, with where it came from and its signature; reflected functions through Reflection.Find |
NotificationPoller heap scans |
ObjectFinder over the engine's object array, falling back to the heap scan |
__try/__except reads |
CodeWriter.TryRead (a guarded read; NativeAOT cannot catch access violations) |
| WinInet / WinHTTP / raw socket | IHttpTransport, implemented on WinHTTP |
The settings are OpenVersus.toml, read and written by Config/TomlConfig.cs through Tomlyn's
syntax tree (its serializer is not used: it drops comments, and its reflection path is not
NativeAOT-safe). Table and key names match regardless of case, as they did in the ini. Writing
only ever adds a missing key, directly under the last key of its table in that table's
Key = Value or Key=Value style, or a missing table at the end; comments, blank lines, spacing,
unknown keys, line endings and a byte-order mark stay as the player left them, and a complete file
is not rewritten at all. A boolean row takes a TOML boolean, or on/off, 1/0 or "true" in
any case. A value that does not parse is logged as a warning, read as its default and left on
disk. A file that is not valid TOML is logged with the line and column where it broke, every
setting takes its default, and the file is left alone. When the settings cannot be used (a TOML
mistake, an unreadable file, an ini that would not convert), a toast after "OpenVersus Loaded" says
so. A missing OpenVersus.toml (or the defaults after an ini that would not
convert) is Config/DefaultConfig.cs: every row in Settings.Table order grouped by table, with
CRLF line endings, [Settings.Debug] just above the servers, and a comment above every setting
and table; a test fails if a row has no comment. Converted and merged files get no comments.
sample.toml is a checked-in copy of that file: the plugin never writes it, and a test holds the
two to the same bytes, so after changing a row, regenerate it by copying the OpenVersus.toml the
plugin writes into an empty folder.
A legacy OpenVersus.ini with no OpenVersus.toml beside it is converted once
(Config/SettingsMigration.cs), line for line: ; comments become #, names take the spelling
of the row they match, values a boolean row reads as a boolean become TOML booleans, and
everything else becomes a string, deprecated and invalid values included. The one exception is both
ServerUrl keys, which become https://prod.openversus.org/ whatever they held, since old files
can still point at the plain-http endpoint that only stays up for them; the log records the old
value. A repeated section or key, or a line that is neither, was never read by the ini reader, and
becomes a comment. The result is checked against the ini reader, key by key, before anything is
written. If it checks out, the TOML is written and the ini deleted; if it does not, the log keeps
the ini's text, the TOML starts from defaults, and the ini is still deleted. The ini is deleted only
once the TOML is on disk. When both files exist (an old ini left beside a TOML, say from a C++
install restored over a converted one), the ini is merged into the TOML
(SettingsMigration.Merge): a row's value is taken only where the TOML is missing it or still has
the default, so nothing changed in the TOML is overwritten; any other key only where the TOML
lacks it; ServerUrl never. Each value taken is logged, with a warning that sums them up, and the
ini is deleted once the TOML is saved. A TOML that does not parse takes nothing, and the ini stays
until it does.
The client's own files are TOML too. OVSState.toml holds [FirstRun] PaidModWarned; an
OVSState.ini is carried over once (the agreement, so the free-mod dialog is not shown again) and
deleted once that is on disk. PatternsCache.toml names the exe (Exe, the top 32 bits of the
.text hash) and client version (Client) it is for at its top, with pattern text to RVA under
[Patterns]; a file for another exe or client, or one that does not parse, starts over. The old
PatternsCache.cache is deleted: it was only a cache. Config/IniFile.cs remains only to read the
old ini files for these conversions; what the TOML side shares with it (boolean spellings, quote
stripping, the atomic write) is in Config/ConfigFile.cs, so dropping ini support one day is
IniFile.cs, SettingsMigration.cs and their tests.
What each key does is documented on its row in Config/Settings.cs (Settings.Rows). Compared
with the C++ client:
- Added:
[Patches] PostMatchFreeze, on by default, with its pattern under[Patterns.MVS]; and, off by default,[Patches] SunsetCallers,[Settings.Debug] CountSunsetCalls,[Settings.Debug] MatchRulesLog(writeslogs/MatchRules.logfor testing the FFA, Individual Stocks and Friendly Fire rules) and[Features] NetStats. Two changes have no setting and no pattern in the file, so a player cannot switch them off: requests to the OpenVersus server carry this install's identity (Hooks/RequestHeadersHook.cs), which ties a player to their own account; and the game's WB Analytics telemetry to event.wbinsights.com is stopped at its send function (Hooks/TelemetryPatch.cs). [Settings] LogLeveltakes a level name as well as a number (see Logs).- No longer read:
LogSize,ModLoader,AntiCheatEngine,CurlSetOptandCurlPerformunder[Settings](Settings.RetiredKeys). They stay in files that have them, each logged at startup with why it is unused; nothing adds them to files that don't. Any other key the client does not know is logged as a warning. [Settings.Keybinds] ToggleMenuis kept so existing files mean what they did, but nothing reads it; the keyboard hook only ever logged F1.
The game's sunset-date check is called thousands of times a minute. [Patches] SunsetDate
(default on) makes the function itself return false with two byte patches. Two more switches,
both off by default:
[Patches] SunsetCallers = truefinds every direct call and tail jump to the function through.pdataand a.textscan, and turns each into "return false" in place, so the function is never entered. The log reports the count found (146 in the final build).[Settings.Debug] CountSunsetCalls = trueroutes the function's comparison path through a counter, and the heartbeat line each minute reports how many calls the last minute saw. With both switches on, that number must be zero.
logs/OpenVersus.log next to the plugin is the running log, truncated at each launch; the
previous run's is kept as logs/OpenVersus_<launch time>.log. Archives from the last week stay as
plain text; older ones are compressed to .zst (zstd level 11, through ZstdSharp) on a
background thread at launch, keeping their timestamps. With [Settings.Debug] ShowConsole on,
every line also goes to the debug console.
If the logs directory cannot be created or written, the log goes to the plugin's own directory
instead; if that fails too, the client still runs, Log.FileError records why, and every line
still reaches the console. Log.Notice says which happened and is the first warning logged.
Log is a Microsoft.Extensions.Logging.ILogger with its own writer (file plus console). The
minimum level comes from [Settings] LogLevel: a name in quotes ("trace", "debug", "info",
"warn", "error", "critical", "none", or the usual aliases such as "verbose", "all",
"err", "quiet") or a number, bare or quoted (1 debug to 6 none; a number past 6 means 6). The
value is tried as a number first, then as a name. An unquoted name (LogLevel = debug) is not
valid TOML, so the whole file falls back to defaults. 0, which converted files carry, means
"decide from [Settings.Debug] DebugLogging": Debug when it is on, Information when it is off. A
name that is not a level is logged and falls back the same way. Tags in the file are TRC DBG NFO WRN ERR CRT.
Per-attempt and game-thread queue lines are Trace; pattern and function resolution is Debug; hooks,
banners and netstats are Information.
With [Features] NetStats = true the per-second NETSTATS lines go to logs/NetStats.log, one
file per match: it opens when a session starts playing and closes at the summary, which archives
it as NetStats_<match start time>_<match id>[_with_<teammates>]_vs_<opponents>.log with the same
retention as the main log. The names come from the player data each fighter pawn carries and the
id from the gameplay-config subsystem (both confirmed on the live game), read at the first playing
sample and, while incomplete, again once a second for ten seconds. A log that never got a
description keeps the plain stamp, and at Debug level the main log lists what each reading saw
(every pointer followed, checked for being an object). A closed match log stays on disk as
NetStats.log and is not archived a second time by the next open.
The main log gets only the session start and end, the match log open and close, and the
NETSTATS-SUMMARY line; anything that used to grep the main log for NETSTATS reads the match
logs now. Between matches the session is looked up through the object array every five seconds,
and every sample asks the array whether the session object is still listed, so a freed session is
never sampled.
- The version is the repository root's
VERSIONand nothing else: the build generatesOvsVersion.Currentfrom it, stamps the assembly and version resource with it and names the plugin for it, andrelease.ymlrefuses a tag that differs from it. - Formatting is
.editorconfig, applied withdotnet format: four spaces, Allman braces, one statement per line. Braces on every control-flow body (IDE0011) is an error in the build; a one-line auto-property or single-expression=>is fine. - Every public member has an XML doc comment. Offsets and addresses say where they came from (a dump, a live read, a disassembly), since a dump's layout is a claim about this build, not a fact.
- Everything takes
ILogger; the short verbs (Info,Warn,Success, ...) are extension methods inLogExtensions.csthat pass the text through verbatim, never as a template. OnlyClientand the plugin entry point hold the concreteLog, since they own its lifetime. - A patch that cannot be made throws
PatchExceptionout ofMemory/(CodeWriter,CallSite,Trampoline);Client.ApplyHookscatches per hook, logs it as that hook's failure and goes on.CodeWriter.TryReadnever throws, since it runs on hot paths. - Settings are rows (
Settings.Rows.*), one staticSettingDefper ini key, read through typed properties;PatternResolver.Find("SigCheck")reads the pattern text from the row of that key. - JSON goes through the source-generated
Net/OvsJson.cscontext (NativeAOT has no reflection for System.Text.Json); add a[JsonSerializable]there for any new shape. - Background loops stop through a
CancellationToken, waited on rather than slept through. - Nothing calls into the engine before
Engine.IsUp(the game instance has existed for three seconds): every entry point inUEandGameUirefuses with an exception until then, and background work waits withEngine.WaitUntilUp. Calling the engine during UE initialization crashes the game with no log line to show for it. - Reads of the game's memory go through
IMemory(ProcessMemoryin the plugin, a byte-backed fake in the tests) and engine name lookups throughIGameNames, so the object array, the finder, and the reflection decoder can be tested on Linux against a synthetic game (OpenVersus.Tests/FakeGame.cs). Those tests prove the logic; the offsets they are built from are the code's own, and only the running game checks those. - What stays C-shaped is what must:
[UnmanagedCallersOnly]hooks and the static state they need, function-pointer casts, sequential-layout structs mirroring the game,nintarithmetic, and Win32 names inNative/.
The version resource (Windows shows it under Properties > Details) carries the product name,
description, Copyright (c) 2026 Christopher Conley, Tuggernuts, the file version from VERSION,
and a product version of <VERSION>+<commit>.<build date, UTC>, all set in
Directory.Build.props. The same text is in OvsVersion (Current, Informational,
Copyright), and the export OpenVersusInfo returns it as a NUL-terminated UTF-8 JSON object
(name, version, productVersion, copyright) for tools that load the plugin on purpose.
Loading it runs nothing: the plugin does no work until the ASI loader calls InitializeASI.
The mod lives in its own folder, plugins/OpenVersus/, which the ASI loader loads like the rest
of plugins/. Everything the plugin writes (settings, state, pattern cache, logs) goes beside it.
Earlier versions lived loose in plugins/, so a plugin running from a folder named OpenVersus
takes over what they left one folder up (Layout.cs): an OpenVersus.toml or OVSState.toml
there is moved in when the mod's folder has none; an OpenVersus.ini or OVSState.ini there is
converted, or merged into an existing TOML, by the same rules as one in the mod's folder; old
pattern caches are deleted. A logs folder is never moved: the C++ client wrote none, and
plugins/logs may belong to another mod.
Every launch, right after the duplicate check, a plugin running from anywhere other than
plugins/OpenVersus/ beside the game executable moves itself there, with its settings, state and
cache files (Layout.MoveInto), and carries on from the new folder; this launch's log stays where
it was opened, and no logs are moved. It does so only when Ultimate ASI Loader would load that
folder (LoaderConfig.cs): the loader reads [GlobalSets] from <its name>.ini, global.ini,
scripts\global.ini, plugins\global.ini and update\global.ini beside it, each overriding the
last, and loads folders under plugins/ when LoadPlugins and LoadRecursively are on, their
default with no settings at all. When either is off, the plugin stays where it is and says why in
the log. Updates install into the same folder the move would use, so they never land where the
loader would not look. If the plugin cannot be moved, nothing is, and it runs where it is.
Ultimate ASI Loader loads every .asi it finds, so a new release extracted over an old one (the
C++ client's OpenVersus.asi, or an older OpenVersus_<version>.asi) would run two clients
patching the same code. The first thing Client.Initialize does is search the plugin's folder
and the game executable's folder, subfolders included, for any other OpenVersus*.asi
(DuplicatePlugins.cs). The newest copy by the version built into it stays, whatever the file is
called: the fixed file version in its version resource, which the build stamps from VERSION,
read from the file's bytes without loading it (loading a copy would run its code, and the C++
client patches the game from DllMain; a copy that crashed while loading would crash the game
before anything was renamed, every launch). A file without a version resource, like the C++
client, counts as oldest, and a tie goes to the running copy. Every other copy, the running one
included if it lost, is renamed to .bak. Then the game closes with a message saying what was
renamed, without patching anything, since another copy may already have patched it this launch.
build.sh --install <game>/plugins (or -Install on Windows) publishes and installs the plugin,
keeping the previous one as a .bak. With DebugLogging = true or LogLevel = "debug",
logs/OpenVersus.log lists every pattern, the address of every function it resolved, and the
hooks that took; when something fails to resolve, try comparing those addresses with the C++ build's
console output.
Set AutoUpdate = false while testing. The update check works against https (the C++ one never
could) and installs any newer release the server offers, which would replace the build under test.
To test the updater itself without GitHub, point [Server.Game] ServerUrl at a local server that
answers GET /ovs/client-version with latest_version and download_url and serves the .asi
and its .sha256; the offered file must really be newer (its version resource decides), or it is
refused.
The Wine harness (build.sh harness or wine-host/run.sh) checks the hooking layer without the
game: the host calls three assembly sites before and after loading the test plugin, which
redirects a call and a jmp into managed code, patches a byte, makes one hook throw to prove the
guard returns the fallback, and swaps a function pointer in read-only data, as in a vtable (and
refuses a second swap whose expected value is stale). It needs wine and mingw-w64 besides the
publish toolchain, and uses its own prefix under local/wine-host.
The former C++ version of the OpenVersus client-side DLL was a fork based on the publicly-available original work of thethiny and multiversuskoth. The current C# version contains no common code whatsoever from the original repository and is an entirely original work.
OpenVersus is not affiliated with, nor endorsed by, WB, PFG, its developers, or any other related entity.
OpenVersus is not affiliated with, nor endorsed by, any other modding project or effort, the developers of those projects, or any other related entity.
This mod is provided on an as-is, sans-warranty basis as detailed in the LICENSE file. Use at your own risk and discretion.