Godot 4 export turns every .tres into a .tres.remap stub, so a DirAccess scan filtered on .tres finds nothing

The item catalogue of a survival game I was building for Android is data-driven: one .tres file per item, and a loader that scans the directory and loads whatever it finds. It worked in the editor, in every test run. On the phone, the same code produced an empty catalogue — no error, no warning, zero items.

TL;DR — On export, Godot 4 converts text resources to binary (editor/export/convert_text_resources_to_binary, default true, invisible in project.godot). Inside the package, data/axe.tres exists on disk only as a stub named data/axe.tres.remap, so DirAccess.get_files_at() returns .remap names and a .tres suffix filter matches nothing. Accept both suffixes, and always give load() the path with .remap trimmed off — load() fails on the .remap path.

The setup

A .tres file is a Godot resource saved in text format — here, plain data: a display name, a weight, an icon. The game keeps one per item type in res://data/items/res:// is Godot’s virtual path to the project’s own files, both in development and inside a package — and a catalogue script scans that directory at startup instead of hard-coding file names:

for file in DirAccess.get_files_at("res://data/items"):
    if not file.ends_with(".tres"):
        continue
    var item := load("res://data/items/" + file)

Adding an item to the game is dropping a file into the directory. Nothing else knows the file names.

What I expected

I thought of export as packaging: files under res:// travel into the APK under their own names, so a directory listing should return the same names as in the source tree. And if the export step did something incompatible with my resources, I expected a loud failure — an error at export time or a load error at runtime, not silence.

What actually happens

I reproduced it with a two-file scratch project on Godot 4.3:

godot --headless --export-pack Linux out.pck
godot --headless --main-pack out.pck

Run from the source tree, the scan behaves as expected:

files on disk:      ["item_a.tres", "item_b.tres"]
filtered on .tres:  ["item_a.tres", "item_b.tres"]  (count: 2)

Run from the exported pack, same code:

files on disk:      ["item_a.tres.remap", "item_b.tres.remap"]
filtered on .tres:  []  (count: 0)
ERROR: Resource file not found: res://data/item_a.tres.remap (expected type: )
   at: _load (core/io/resource_loader.cpp:288)
load(res://data/item_a.tres.remap) -> <Object#null>
load(res://data/item_a.tres) -> <Resource#-9223372011269520058>

Three observations in one screen. The physical file names changed. load() on the name that is actually on disk fails. And load() on the original .tres path — a file that no longer exists under that name — works.

The real APKs agree. Listing the game’s release APK with unzip -l gave 66 entries ending in .tres.remap and zero ending in .tres; a second, unrelated game’s APK gave 239 and zero. Each stub is a tiny text file — 98 bytes for this one, a location definition:

[remap]

path="res://.godot/exported/133200997/export-4814f3cc51a03fb56a9cfee73e548691-epave.res"

The real data lives at that hashed path, converted to Godot’s binary resource format.

In the editor, a directory scan filtered on .tres finds the item files and loads them; in the exported APK the same files exist only as .tres.remap stubs pointing into .godot/exported/, so the filter keeps zero files and the game gets empty data without any error.

The fix

Accept both suffixes in the scan, and trim .remap before loading:

const DATA_DIR := "res://data/items"

func load_all() -> Array[Resource]:
    var items: Array[Resource] = []
    for file in DirAccess.get_files_at(DATA_DIR):
        if not (file.ends_with(".tres") or file.ends_with(".tres.remap")):
            push_warning("unexpected file in %s: %s" % [DATA_DIR, file])
            continue
        items.append(load(DATA_DIR + "/" + file.trim_suffix(".remap")))
    return items

trim_suffix(".remap") is a no-op in the editor and restores the loadable path in a package, so one branch covers both worlds. The push_warning branch is deliberate: a mistyped extension like axe.tre should stay loud, not vanish into the same silence that caused this bug. That failure shape — a filter that quietly drops valid entries instead of erroring — is the same one I ran into in a solver that pruned moves by event type.

Why it works

editor/export/convert_text_resources_to_binary defaults to true and only exists at export time, so it never appears in project.godot unless someone changed it — nothing in the repository hints that the setting exists. On export, each text resource is converted to binary and stored under res://.godot/exported/<n>/export-<hash>-<name>.res; the original path keeps only the [remap] stub shown above.

The two APIs then disagree about what a file is called. load() and ResourceLoader speak logical names: given the original .tres path they notice the stub and follow it to the binary file, which is why hard-coded load("res://x.tres") calls never break. DirAccess speaks physical names: it lists what is actually inside the package, stubs included. A scan-then-filter loader sits exactly on that disagreement, and the filter turns it into empty data instead of an error. Scenes have the same conversion (.tscn becomes .scn behind a remap), but almost nobody discovers it there, because scenes are loaded by explicit paths.

What I did not test

  • Whether disabling the setting in the project settings avoids the renaming. The docs say the conversion shrinks files and speeds up loading, so I kept it on and fixed the scan instead.
  • ResourceLoader.list_directory() (Godot 4.4+) as an alternative to DirAccess. The game I fixed avoided it after observing that it pre-filters to recognized resource types, hiding mistyped files; I did not measure how it names files in a package.
  • iOS or desktop export templates. I verified a Linux PCK (Godot 4.3) and two Android APKs (built with 4.7); the conversion happens per export, not per platform, and both platforms I checked behaved identically.

Facts

context: Godot 4 game loading gameplay data by scanning a res:// directory for .tres files with DirAccess.get_files_at()
problem: in any exported package (APK, PCK) each .tres exists only as a .tres.remap stub, so a ".tres" suffix filter matches zero files — empty data, no error, while the editor run works
solution: accept both ".tres" and ".tres.remap" in the scan and call load() with the ".remap" suffix trimmed; load() on a .remap path returns null with "Resource file not found"
verified_on: 2026-08-17
applies_to: [Godot 4.3, Godot 4.7, Android APK export, PCK export]
does_not_apply_to: [runs from the source tree (files keep .tres), resources loaded via hard-coded paths, Godot 3.x (not tested)]

1 réflexion sur “Godot 4 export turns every .tres into a .tres.remap stub, so a DirAccess scan filtered on .tres finds nothing”

  1. Ping : Godot 4 parses every JSON number as a float, and Array.has(22) misses a stored 22.0

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Retour en haut