Zum Hauptinhalt springen
Inoffizieller Guild Wars Reforged App — ein Fan-Projekt, nicht mit ArenaNet oder NCSoft verbunden

UR-01 to UR-04: unimplemented Emscripten file routines

Four routines in Base/Os/Emscripten ship as stubs. They disable template save and list, screenshots, and chat logs. One of them aborts the client.

Four routines in Base/Os/Emscripten ship without an implementation. Three return a constant; one calls assert and then unreachable. Between them they disable skill and equipment templates, the template list, screenshots, and chat logs — and the fourth one aborts the client.

IDRoutineTodayFix
UR-01Directory creationReturns error 2 unconditionallyFS.mkdirTree
UR-02Directory enumerationEmpty body, every directory reads as emptyFS.readdir
UR-03Entry-name derivationReturns 0, writes nothingPath arithmetic
UR-04File deletionassert and unreachableFS.unlink

Build used for the measurements below: b0319704f3072d6948a66026a35af5eb0af12b48d70986783c293e7c77e98483.

What the player sees

ActionResult today
Save a skill template"The attempt to save the Skills Template named … failed because the file could not be created/written to"
Open "Load from Skills Template"The list is always empty, also after a successful save
Rename a template"The attempt to rename the Skills Template named … failed"
Delete a templateThe client stops
Take a screenshotNo file appears in Screens/
Write a chat logNo file appears

UR-01 — directory creation always fails

Function 404, signature (wchar_t* path, int recursive) -> int, in Base/Os/Emscripten/EmscriptenPath.cpp.

Fix: low effort, small blast radius — one clear task with a working Windows equivalent, and four call sites that all expect this behaviour already.

What the client does now

The complete function body is two instructions:

wasm
i32.const 2
end

It returns 2 (ERROR_FILE_NOT_FOUND) every time, without touching the Emscripten file system. We called the shipped function directly:

func_404(L"Templates/Skills/", 1) = 2

All callers treat a non-zero result as a failure of the whole operation:

CallerPurpose
9757, Gw/Account/Cli/AcctCliTemplate.cppCreate Templates/Skills or Templates/Equipment before a write
12433, Gw/Ui/UiScreen.cppCreate Screens before a screenshot
11744, CtChatLog.cppCreate the chat-log directory
11460, ActDlgLogin.cppThe result is discarded

The template save path checks this result before opening the file, so AcctCliTemplate.cpp:1020 stores error 2 and the client reports that it can't create the file — without ever reaching the file system. We confirmed this: the save failure occurs with zero openat activity.

Expected behaviour

Create the directory, including parent directories when recursive is set. Return 0 on success and the matching error code on failure.

The module imports no mkdir. FS.mkdirTree in the Emscripten file-system API is the direct equivalent.

Acceptance criteria

  • func_404(L"Templates/Skills/", 1) returns 0 on a mount where Templates/ does not exist yet.
  • After the call, Templates/Skills exists and is a directory.
  • A second call on the same path returns 0 and changes nothing.
  • A call with recursive = 0 on a path whose parent is absent returns a non-zero error code.
  • A screenshot creates a file in Screens/.

UR-02 — directory enumeration does nothing

Function 405, signature (FileList* out, const wchar_t* pattern, int flags) -> void, same translation unit.

Fix: medium effort — the pattern match and the allocation contract need care. Small blast radius: the callers already handle a list of any length.

What the client does now

The function body is empty — it returns without writing to out. The caller zeroes out before the call, so every directory reads as empty.

The output structure, taken from the call sites:

c
struct FileList {          // 16 bytes, the caller zeroes this
    Entry*   entries;      // +0   the caller frees this with free()
    uint32_t _pad;         // +4
    uint32_t count;        // +8
    uint32_t _pad2;        // +12
};

struct Entry {             // 544 bytes, stride confirmed at every call site
    uint8_t  header[24];   // +0   no caller reads this
    wchar_t  name[260];    // +24  file name, with extension
};

The flags parameter selects the entry kind. We observed these values:

CallerPatternflags
9747, AcctCliTemplate.cppTemplates/Skills/*.txt17 — files
9746, AcctCliTemplate.cppTemplates/Skills/*18 — directories
11744, CtChatLog.cpp<dir>/gw???.txt1
12433, UiScreen.cppScreens/gw???.???1

Bit 0 selects files, bit 1 selects directories. Bit 4 shows up only in the template scans; we didn't determine its meaning.

Expected behaviour

Write the matching entries into out, setting entries and count.

The module imports no getdents. Use FS.readdir and FS.stat.

Allocation contract
The caller frees entries, so the block has to come from the client's own allocator. Don't allocate it in the host or in a different heap.

Acceptance criteria

  • A directory with three matching files gives count = 3.
  • Each entry holds the file name with its extension at offset +24.
  • The entry stride is 544 bytes.
  • flags = 1 returns files only. flags = 2 returns directories only.
  • * and ? match inside one path component.
  • The caller can call free(out->entries) without a fault.
  • "Load from Skills Template" lists a template that the player saved before.

UR-03 — entry-name derivation writes nothing

Function 416, signature (wchar_t* dst, int, const wchar_t* baseDir, int, const wchar_t* path, int dstChars) -> int, same translation unit.

Fix: low effort. Medium blast radius — the two model paths flip from their fallback branch to the main branch after the fix; test them together with this change.

What the client does now

The complete function body is two instructions:

wasm
i32.const 0
end

It returns 0 and writes nothing to dst.

Callers: 9746 and 9747 (AcctCliTemplate.cpp, both template scans), 4463 (MdlDecomp.cpp), and 4532 (MdlTex.cpp).

In the template scans the client drops the result and then reads dst anyway, so it ends up cleaning and registering uninitialised stack memory as the template name. The two model paths test the result and take their fallback branch.

Expected behaviour

Write path relative to baseDir into dst and return the number of characters written.

In the template scans, baseDir is Templates/Skills/ and path is an entry name from UR-02.

Acceptance criteria

  • With baseDir = L"Templates/Skills/" and path = L"Templates/Skills/My.txt", dst holds the relative name and the return value is non-zero.
  • The function writes at most dstChars characters.
  • The two model paths keep their current behaviour when the derivation fails.

UR-04 — file deletion stops the client

Function 552, signature (const wchar_t* path) -> int, in Base/Os/Emscripten/Exe/EmscriptenExeFile.cpp, line 840.

Fix: low effort, small blast radius — one live call path.

What the client does now

The complete function body is an assertion and a trap:

wasm
i32.const "not implemented"
i32.const "../../../../Base/Os/Emscripten/Exe/EmscriptenExeFile.cpp"
i32.const 840
call <assert>
unreachable

The call path is File::Delete → 730 → 678 → 552. The caller reads a non-zero result as success and reports error 4 otherwise.

A delete therefore stops the client. A rename stops it too, because a rename is two steps — write the new name, then delete the old one. When the write step fails first, the client reports a rename failure instead.

Expected behaviour

Delete the file. Return non-zero on success, zero on failure.

The module imports no unlink. Use FS.unlink.

Acceptance criteria

  • A delete of an existing template removes the file and returns non-zero.
  • A delete of an absent file returns zero and does not stop the client.
  • A rename of a template completes, and the old file is gone.
  • No assert call remains on this path.
Ten more unimplemented bodies nearby

The same translation unit holds eight more assert("not implemented") bodies: functions 526, 527, 532, 533, 534, 535, 538, and 539, at lines 490, 601, 608, and 634. Net/DiagLib/Emscripten/EmscriptenDiagLib.cpp holds two more, at lines 23 and 28.

None of them is reachable today — the eight are vtable slots with no live caller, and the DiagLib pair sits behind a disabled flag. We list them in case they're useful for planning.


How to reproduce

These steps need a stock client. No host change is needed.

Save a template

Log in and open the skills panel. Save a build under any name. The save fails. This is UR-01.

Open the template list

Open "Load from Skills Template". The list is empty. This is UR-02 and UR-03.

Delete a template

This step needs a build in which UR-01 and UR-02 work. Delete a listed template. The client stops. This is UR-04.

Our workaround today

The host rewrites the module before starting the client: it appends five forwarder functions and repoints the stub call sites at them. Each forwarder calls out to the host through a marker value on an existing syscall import, and the host does the actual file work in the Emscripten file system.

That rewrite has to be re-certified for every new client build. Implementing these four routines makes it unnecessary — our workarounds has the detail.

GWonMac

Spiele Guild Wars auf deinem Mac. GWonMac führt ArenaNets offiziellen Client nativ auf Apple Silicon aus — kostenlos, Open Source, signiert und notarisiert.

© GWonMac. Alle Rechte vorbehalten.