9.6 KiB
Testing
Linux headless
m2pack-secure can be validated headless on a Linux VPS without GUI support.
This covers the archive toolchain and release artifacts, not the full Windows client runtime.
Run:
cmake -S . -B build
cmake --build build -j
python3 scripts/headless_e2e.py
Or through npm:
npm run headless:e2e
The headless E2E test covers:
keygenbuildlistverifydiffexport-client-configexport-runtime-keyinjsonexport-runtime-keyinblobextract- byte-for-byte comparison of extracted files
What it does not cover
- Windows SDK compilation of the client
- Direct3D startup
- shared-memory launcher bootstrap on Windows
- in-game runtime behavior of the client loader
Full client runtime
To test the actual client runtime you still need one of these:
- a Windows build machine
- a Windows VM
- or a Linux host with
wineplus the required runtime dependencies
The Linux VPS path is now validated as well.
Confirmed runtime path on this host:
- Linux-hosted Windows build of
m2dev-client-src - headless execution through
xvfb-run + wine - runtime key delivery through:
M2PACK_MASTER_KEY_HEXM2PACK_KEY_ID
Confirmed .m2p smoke tests:
root.m2ponly, withroot.pckremovedroot.m2p+patch1.m2p, with both legacy.pckfiles removedroot.m2p+patch1.m2p+season3_eu.m2p, with all three legacy.pckfiles removedroot.m2p+patch1.m2p+patch2.m2p+season3_eu.m2p, with all four legacy.pckfiles removedroot.m2p+patch1.m2p+patch2.m2p+season3_eu.m2p+metin2_patch_snow.m2p+metin2_patch_snow_dungeon.m2p+metin2_patch_etc_costume1.m2p+metin2_patch_pet1.m2p, with all eight matching legacy.pckfiles removedroot.m2p+patch1.m2p+patch2.m2p+season3_eu.m2p+metin2_patch_snow.m2p+metin2_patch_snow_dungeon.m2p+metin2_patch_etc_costume1.m2p+metin2_patch_pet1.m2p+metin2_patch_pet2.m2p+metin2_patch_ramadan_costume.m2p+metin2_patch_flame.m2p+metin2_patch_flame_dungeon.m2p, with all twelve matching legacy.pckfiles removed- the same twelve-pack startup set plus:
locale.m2puiscript.m2puiloading.m2pETC.m2pwith all sixteen matching legacy.pckfiles removed
- the same sixteen-pack startup and UI bootstrap set plus:
item.m2peffect.m2picon.m2pproperty.m2pwith all twenty matching legacy.pckfiles removed
- the same twenty-pack set plus:
terrain.m2ptree.m2pzone.m2poutdoora1.m2poutdoora2.m2poutdoorb1.m2poutdoorc1.m2poutdoorsnow1.m2pwith all twenty-eight matching legacy.pckfiles removed
- the same twenty-eight-pack set plus:
pc.m2ppc2.m2pguild.m2pnpc.m2pmonster2.m2psound.m2psound_m.m2psound2.m2pwith all thirty-six matching legacy.pckfiles removed
- the same thirty-six-pack set plus:
monster.m2pnpc2.m2ptextureset.m2poutdoora3.m2poutdoorb3.m2poutdoorc3.m2poutdoordesert1.m2poutdoorflame1.m2poutdoorfielddungeon1.m2pwith all forty-five matching legacy.pckfiles removed
In every confirmed case the client reached the login bootstrap path under Wine and completed:
PackInitialize- Python bootstrap through
prototype.py app.Create(...)app.SetCamera(...)MainStream.SetLoginPhase()
This is now enough to treat the .m2p path as validated for the current core
startup, UI bootstrap, and shared login-adjacent content pack set used by the
client runtime.
At this point the login-phase smoke test stops being a strong validator for additional packs such as world, NPC, monster, and late-load gameplay content. Those should be validated with map loads or in-game scenario coverage rather than startup-only checks.
The world, audio, actor, and gameplay-adjacent packs listed above are therefore
validated only as
startup-time regression smoke coverage. They do prove that the additional
.m2p archives do not break bootstrap or early asset resolution, but they do
not yet prove full map streaming, actor loading, or gameplay correctness.
Current non-fatal runtime issues on the VPS:
- missing
Tahomafont mapping in the client runtime - headless ALSA / audio decoder warnings
Those do not currently block .m2p loading or the login-phase smoke test.
Example runtime command:
M2PACK_MASTER_KEY_HEX="$(cat /home/mt2.jakubkadlec.dev/.secrets/m2pack-secure/2026-04-14/master.key)" \
M2PACK_KEY_ID=1 \
M2_TIMEOUT=20 \
WINEDEBUG=-all \
/home/mt2.jakubkadlec.dev/metin/repos/m2dev-client-src/scripts/run-wine-headless.sh \
/home/mt2.jakubkadlec.dev/metin/repos/m2dev-client-src/build-mingw64-lld/bin
Automated smoke runner for selected packs:
python3 scripts/runtime_smoke_wine.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--client-repo /home/mt2.jakubkadlec.dev/metin/repos/m2dev-client-src \
--master-key /home/mt2.jakubkadlec.dev/.secrets/m2pack-secure/2026-04-14/master.key \
--key-id 1 \
--pack root \
--pack patch1 \
--pack patch2 \
--pack season3_eu \
--pack locale \
--pack uiscript \
--pack uiloading \
--pack ETC \
--json
World/map scenario validator:
python3 scripts/validate_runtime_scenarios.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--json
This validator checks real cross-pack world references:
- each selected
Outdoor*map pack containssetting.txt - each selected
Outdoor*map pack containsmapproperty.txt TextureSettargets fromsetting.txtexist intexturesetEnvironmenttargets fromsetting.txtexist inETC/ymir work/environment
Actor/content scenario validator:
python3 scripts/validate_actor_scenarios.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--json
This validator checks local actor integrity for Monster, NPC, and PC:
motlist.txtmotion files exist- each motion has a paired
.gr2in the same actor directory .msmbase model targets resolve against the runtime asset set.msmeffect script targets resolve against the runtime asset set.msmdefault hit effect targets resolve against the runtime asset set.msmdefault hit sound targets resolve against the runtime asset set
Effect graph validator:
python3 scripts/validate_effect_scenarios.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--json
This validator checks text-based effect assets in Effect:
.mseparticleTextureFiles.msemeshmeshfilename.msfBombEffect.msfAttachFile- derived
.msssound scripts and their referenced.wavfiles
Runtime release gate:
python3 scripts/validate_runtime_gate.py \
--runtime-root /tmp/m2dev-client-runtime-http
Strict runtime release gate:
python3 scripts/validate_runtime_gate.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--strict-known-issues
The gate runs these validators together:
scripts/validate_runtime_scenarios.pyscripts/validate_actor_scenarios.pyscripts/validate_effect_scenarios.pyscripts/validate_audio_scenarios.py
By default they load the shared baseline:
known_issues/runtime_known_issues.json
Result semantics:
known_issue_ids: currently accepted historical content issuesunexpected_issue_ids: new issues that fail the gatestale_known_issue_ids: baseline entries not observed anymore
Default behavior:
- known issues are reported but do not fail the gate
- only unexpected issues fail the gate
Strict behavior:
- unexpected issues fail the gate
- stale known-issue entries also fail the gate
Current baseline on the real runtime:
world:0actor:5effect:12audio:1
Audio scenario validator:
python3 scripts/validate_audio_scenarios.py \
--runtime-root /tmp/m2dev-client-runtime-http \
--json
This validator checks the runtime audio script layer:
- all
*.mssfiles under the real client runtime - every
SoundDataNNreference towav/mp3 - resolution against the effective virtual audio namespace used by the client
Current real-runtime findings now show a single historical audio content issue:
sound2/sound/pc2/assassin/dualhand_sword/combo7.wav. That remaining issue is
recorded in the shared runtime baseline and does not fail the gate unless it
changes.
Built-in CI:
.gitea/workflows/ci.ymlbuildsm2packon Linux- the CI workflow runs
scripts/headless_e2e.py - the real client runtime gate remains a separate step because it depends on external client assets not stored in this repository
Self-hosted runtime CI:
.gitea/workflows/runtime-self-hosted.ymlscripts/self_hosted_runtime_ci.py
This path is meant for a dedicated runner that already has:
- a real client runtime checkout
- a Linux-built
m2dev-client-src - Wine runtime dependencies
- access to the runtime master key file
Expected runner environment:
M2_RUNTIME_ROOTM2_CLIENT_REPOM2_MASTER_KEY_PATHM2_KEY_ID- optional
M2_RUNTIME_SMOKE_PACKS - optional
M2_TIMEOUT
The self-hosted orchestrator runs, in order:
scripts/headless_e2e.pyscripts/validate_runtime_gate.pyscripts/runtime_smoke_wine.py
Default Wine smoke coverage uses the known startup-safe .m2p pack set:
rootpatch1patch2season3_eumetin2_patch_snowmetin2_patch_snow_dungeonmetin2_patch_etc_costume1metin2_patch_pet1metin2_patch_pet2metin2_patch_ramadan_costumemetin2_patch_flamemetin2_patch_flame_dungeonlocaleuiscriptuiloadingETCitemeffecticonproperty