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.
| ID | Routine | Today | Fix |
|---|---|---|---|
UR-01 | Directory creation | Returns error 2 unconditionally | FS.mkdirTree |
UR-02 | Directory enumeration | Empty body, every directory reads as empty | FS.readdir |
UR-03 | Entry-name derivation | Returns 0, writes nothing | Path arithmetic |
UR-04 | File deletion | assert and unreachable | FS.unlink |
Build used for the measurements below:
b0319704f3072d6948a66026a35af5eb0af12b48d70986783c293e7c77e98483.
What the player sees
| Action | Result 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 template | The client stops |
| Take a screenshot | No file appears in Screens/ |
| Write a chat log | No 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:
i32.const 2
endIt 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) = 2All callers treat a non-zero result as a failure of the whole operation:
| Caller | Purpose |
|---|---|
9757, Gw/Account/Cli/AcctCliTemplate.cpp | Create Templates/Skills or Templates/Equipment before a write |
12433, Gw/Ui/UiScreen.cpp | Create Screens before a screenshot |
11744, CtChatLog.cpp | Create the chat-log directory |
11460, ActDlgLogin.cpp | The 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)returns0on a mount whereTemplates/does not exist yet.- After the call,
Templates/Skillsexists and is a directory. - A second call on the same path returns
0and changes nothing. - A call with
recursive = 0on 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:
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:
| Caller | Pattern | flags |
|---|---|---|
9747, AcctCliTemplate.cpp | Templates/Skills/*.txt | 17 — files |
9746, AcctCliTemplate.cpp | Templates/Skills/* | 18 — directories |
11744, CtChatLog.cpp | <dir>/gw???.txt | 1 |
12433, UiScreen.cpp | Screens/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.
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 = 1returns files only.flags = 2returns 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:
i32.const 0
endIt 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/"andpath = L"Templates/Skills/My.txt",dstholds the relative name and the return value is non-zero. - The function writes at most
dstCharscharacters. - 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:
i32.const "not implemented"
i32.const "../../../../Base/Os/Emscripten/Exe/EmscriptenExeFile.cpp"
i32.const 840
call <assert>
unreachableThe 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
assertcall remains on this path.
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.