Files
m2pack-secure/docs/testing.md
server 65a8e243d3
Some checks failed
ci / headless-e2e (push) Has been cancelled
runtime-self-hosted / runtime-ci (push) Has been cancelled
Clean actor runtime baseline after motion fix
2026-04-14 19:22:49 +02:00

9.5 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:

  • keygen
  • build
  • list
  • verify
  • diff
  • export-client-config
  • export-runtime-key in json
  • export-runtime-key in blob
  • extract
  • 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 wine plus 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_HEX
    • M2PACK_KEY_ID

Confirmed .m2p smoke tests:

  • root.m2p only, with root.pck removed
  • root.m2p + patch1.m2p, with both legacy .pck files removed
  • root.m2p + patch1.m2p + season3_eu.m2p, with all three legacy .pck files removed
  • root.m2p + patch1.m2p + patch2.m2p + season3_eu.m2p, with all four legacy .pck files removed
  • root.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 .pck files removed
  • root.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 .pck files removed
  • the same twelve-pack startup set plus:
    • locale.m2p
    • uiscript.m2p
    • uiloading.m2p
    • ETC.m2p with all sixteen matching legacy .pck files removed
  • the same sixteen-pack startup and UI bootstrap set plus:
    • item.m2p
    • effect.m2p
    • icon.m2p
    • property.m2p with all twenty matching legacy .pck files removed
  • the same twenty-pack set plus:
    • terrain.m2p
    • tree.m2p
    • zone.m2p
    • outdoora1.m2p
    • outdoora2.m2p
    • outdoorb1.m2p
    • outdoorc1.m2p
    • outdoorsnow1.m2p with all twenty-eight matching legacy .pck files removed
  • the same twenty-eight-pack set plus:
    • pc.m2p
    • pc2.m2p
    • guild.m2p
    • npc.m2p
    • monster2.m2p
    • sound.m2p
    • sound_m.m2p
    • sound2.m2p with all thirty-six matching legacy .pck files removed
  • the same thirty-six-pack set plus:
    • monster.m2p
    • npc2.m2p
    • textureset.m2p
    • outdoora3.m2p
    • outdoorb3.m2p
    • outdoorc3.m2p
    • outdoordesert1.m2p
    • outdoorflame1.m2p
    • outdoorfielddungeon1.m2p with all forty-five matching legacy .pck files 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 Tahoma font 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 contains setting.txt
  • each selected Outdoor* map pack contains mapproperty.txt
  • TextureSet targets from setting.txt exist in textureset
  • Environment targets from setting.txt exist in ETC/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.txt motion files exist
  • each motion resolves to a valid .gr2, including redirected MotionFileName targets in .msa
  • .msm base model targets resolve against the runtime asset set
  • .msm effect script targets resolve against the runtime asset set
  • .msm default hit effect targets resolve against the runtime asset set
  • .msm default 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:

  • .mse particle TextureFiles
  • .mse mesh meshfilename
  • .msf BombEffect
  • .msf AttachFile
  • derived .mss sound scripts and their referenced .wav files

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.py
  • scripts/validate_actor_scenarios.py
  • scripts/validate_effect_scenarios.py
  • scripts/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 issues
  • unexpected_issue_ids: new issues that fail the gate
  • stale_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: 0
  • actor: 0
  • effect: 12
  • audio: 0

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 *.mss files under the real client runtime
  • every SoundDataNN reference to wav/mp3
  • resolution against the effective virtual audio namespace used by the client

Current real-runtime findings no longer show any audio content issues. The audio validator is now expected to pass cleanly on the current runtime.

Built-in CI:

  • .gitea/workflows/ci.yml builds m2pack on 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.yml
  • scripts/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_ROOT
  • M2_CLIENT_REPO
  • M2_MASTER_KEY_PATH
  • M2_KEY_ID
  • optional M2_RUNTIME_SMOKE_PACKS
  • optional M2_TIMEOUT

The self-hosted orchestrator runs, in order:

  1. scripts/headless_e2e.py
  2. scripts/validate_runtime_gate.py
  3. scripts/runtime_smoke_wine.py

Default Wine smoke coverage uses the known startup-safe .m2p pack set:

  • root
  • patch1
  • patch2
  • season3_eu
  • metin2_patch_snow
  • metin2_patch_snow_dungeon
  • metin2_patch_etc_costume1
  • metin2_patch_pet1
  • metin2_patch_pet2
  • metin2_patch_ramadan_costume
  • metin2_patch_flame
  • metin2_patch_flame_dungeon
  • locale
  • uiscript
  • uiloading
  • ETC
  • item
  • effect
  • icon
  • property