A new project that is named after your game - #88
Merged
Conversation
Every project this editor has ever made had a module called BlankGame in it. NewProjectDialog handed CopyModule the same id for source and target, so the engine's rename path never ran, and the game module kept the template's name, the template's Author of nothing, and the template's description -- "A blank game, ready for you to craft into something amazing!" -- no matter what the person creating it typed. The dialog asked for three things and only the title reached anything. It asks for six now: Title, Directory, Game Core, Module Name, Author and Description, each with a tooltip on the caption as well as on the input. Module Name defaults to the title stripped to identifier characters plus "Game", and it follows the title until the moment someone types their own, after which it is theirs. Description now reaches the game module as well as AppCore, and Author reaches it at all. RENAMING A MODULE IS NOT RENAMING ITS ModuleId. The engine calls <ModuleId>::<CreateFunction>, so the id in module.taml and the namespace in the module's script are the same name written twice, and asset ids are that name a third time -- "<ModuleId>:<assetName>", in script and in every taml that references an asset. Rewrite only module.taml and the module still loads, still reports its new name everywhere the UI looks, and silently does nothing: create never fires, and its assets resolve to a module that no longer exists. Nothing about the definition it wrote looked wrong, which is why this went unnoticed. So the rename is a pass over the module's own source, and it lives in one place, ModuleStamper, called from all three paths that rename a module. The other two were worse than the New Project one, because a person could reach them repeatedly: New Module from a template (ProjectGamePanel::onModuleCreated) and Edit Module changing a name (::onModuleEdited) both rewrote ModuleID and stopped there. The substitution is whole-word -- a match counts only where neither neighbour can be part of an identifier -- because in the Edit Module case the old id is whatever a person called their module, and can be a substring of an ordinary word in a comment. The engine has half of this already and it is not enough. copyModule runs a TamlModuleIdUpdateVisitor when the ids differ, but that visitor is root-only, so an asset id on a nested element is missed; it cannot touch .cs at all, which is where the namespace lives; and it renames module.taml to <ModuleId>.module.taml, a name that nothing else in the editor opens a module definition by, with no fileRename bound to script to put it back. Hence the copy going out under the template's own id and the rename happening afterwards, on the copy. TEMPLATE IS NOW A FLAG AND Type SAYS WHAT A MODULE IS. Type == "Template" was only ever read as an enumeration filter, in the two places that either wanted templates or did not; "leave this module alone" is enforced by the engine's own Synchronized field and always was. So BlankGame is Type="Game Core" and ArtPack is Type="Art Pack", both carrying Template="1" and a DisplayName, the two filters test the flag, and the Game Core dropdown can ask findModuleTypes for exactly the templates a project can be built out of. The markers come off the copy. Two fixes to what was already there, both found by the tests. The New Project dialog appended a trailing backslash to its project path and then built every other path on top of it, which the engine's own file calls expand away and script-side isDirectory does not -- it stats the string it is given, so ModuleStamper was handed a folder that did not exist. And the buttons were positioned from the dialog's own height rather than from EditorDialog:: contentHeight, putting the Create button eight pixels past the fold and the whole form behind a scroll bar; the comment on contentHeight names this exact trap and lists two other dialogs that shipped with it. Both suites end by loading the renamed module and checking its gui reached the Canvas, because that is the only thing that proves create actually fired. The layout checks assert every control ends inside the content pane, and they fail on the code that shipped a scroll bar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make a project and its game module declares Path="Sprites" and Path="Fonts", where the template it was copied from said "sprites" and "fonts". On Windows nobody notices. On Linux those directories do not exist, so an image dropped into sprites/ afterwards is never scanned and never becomes an asset -- silently, with the module.taml sitting right there looking correct. AppCore gets it too, so every project made by this editor has had it. Nothing in the path code was wrong. The fields were TypeString, whose ConsoleSetType interns without caseSens, and StringTable's hash is case insensitive by construction: hashString runs every byte through a to-lower table, so two spellings that differ only in case ALWAYS land in the same bucket, and an insensitive insert then matches the first one there with dStricmp and hands back ITS spelling rather than the one it was asked for. Whether a project came out right depended on whether anything else in the process had already interned "Sprites" -- which is why this looked intermittent and why the template on disk was never wrong. TypeCaseString already existed and already does the right thing -- insert(argv[0], true) -- and is already used on taml serialized classes (GuiControl::text), so this is a field type change rather than new machinery. Declared and Referenced assets take it for Path and Extension; ModuleMergeDefinition for MergePath. ModuleDefinition's ScriptFile and AssetTagsManifest are protected fields, so the setter is the write path and takes the flag directly. The dead C++ setters beside them were changed too, so the classes cannot disagree with themselves later. WHAT IS DELIBERATELY LEFT FOLDED. ModuleId, Group, Type and the create and destroy function names stay TypeString. Those are identifiers rather than paths, and unlike the paths they ARE compared as string table pointers -- ModuleManager does it in nine places for load order, groups, types and dependency resolution. That matching has always been case insensitive, so a project depending on "AppCore=1" resolves against a module whose definition says "appCore" today. Making them case sensitive would break that, and it would present as a missing module rather than as a spelling problem. There is a test asserting ModuleId still folds, so this reads as a decision. Every field changed here was checked for pointer comparison first, because that is the way this fix bites back: StringTableEntry equality is pointer equality, and it only holds because both sides were interned the same way. Once a bucket holds one spelling insensitively and another sensitively, an insensitive insert returns whichever node sits earlier in the chain, so a case sensitive field compared against a value interned the old way can miss. None of these seven are compared -- getPath only ever feeds a dSprintf, getExtension is matched with dStricmp inside the scan, and ScriptFile, AssetTagsManifest and MergePath are only ever tested against EmptyString before being formatted into a path. The ResourceManager and TextureManager dictionaries are the reason the fix stops here. Both HASH by pointer value, so a mismatch does not merely fail a comparison, it never reaches the right bucket at all -- and their keys come from the platform layer's dumpPath, which interns whatever readdir returned. That cluster is self consistent today and has to move as one piece with every platform back end. Worth doing, not worth doing halfway. stringTableCaseTests pins the primitive down in both directions, including the hazard above, so that a later simplification of the flag fails a test that says why it exists. declaredPathCaseTests goes through setDataField rather than the C++ setters, because that is the route TAML takes, and it carries a control: ModuleId is poisoned and set identically and still folds, which is what makes the six passing cases evidence about the field type rather than about the fixture. The New Project suite checks the artifact -- Path="sprites", ScriptFile="game.cs" -- case sensitively, against a project the editor really built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BlankGame declares eight asset paths and shipped three of them, so every project made from it warned four times the first time it was opened -- about sprites, fonts and particles, directories the template never had. Adding them is the whole of the content change here, and each one carries a readme saying what belongs in it and how a file there becomes an asset, because a template is something a person opens and reads. Adding them turned up the rest of this commit. A directory called fonts came back out of readdir as "Fonts". Not sometimes: always, on this platform, since the string table's hash is case insensitive by construction and an ordinary insert returns whichever spelling of a name reached the table first. Two of them get there during STATIC INITIALISATION, before any of the engine runs -- SpriteBatch interns "Sprites" as a taml node name and guiProfileTheme interns "Fonts" as a field group -- so sprites and fonts, the two most ordinary names an asset folder can have, were the two that could never survive a scan. That is why a copied module.taml said Path="Sprites", and why directoryDelete could not recurse into a folder it had just been told about: it was handed a name nothing could stat. So the platform layer interns what readdir gives it case sensitively, and so do ResManager::getPaths, the zip and openFileForWrite paths, and the ResDictionary's own path normalisation. THOSE HAD TO MOVE TOGETHER. The dictionary derives its bucket from the POINTER VALUES of path and name and compares them by pointer, so a half-applied change does not merely compare false -- it hashes into the wrong bucket and reports a file that exists as missing. The four in resourceDictionary are the path key, the rest are the file key, and either one alone would have been worse than the bug. The four editor themes are part of this rather than incidental to it. Each asked for "^EditorCore/Themes/<name>/Fonts" while the directory on disk is fonts, and that is where $GUI::fontCacheDirectory comes from. It worked only because the fold rescued it -- guiProfiles.cs asks for the same LabCoat directory in lower case two files away, which is the codebase disagreeing with itself. Left alone, they would have become a real missing-font-cache bug the moment lookups stopped folding, so a latent bug and its disguise are removed in one go. WHAT IS STILL DELIBERATELY FOLDED is unchanged from the previous commit: ModuleId, Group, Type and the create and destroy function names are identifiers, they are compared as string table pointers in nine places in ModuleManager, and dependency resolution has always matched them regardless of case. directoryScanCaseTests writes a real sprites/readme.md and fonts/readme.md, puts the capitalised spellings in the table by hand so the collision cannot be absent by luck, and then asks for the one thing all of this is about: that a caller can open what the scan named. Its last two lines delete the tree the scan just enumerated, which is the exact step that failed in the wild. Only Linux is changed. The same intern sites exist in all six platform back ends and are wrong in the same way, but Windows and macOS are case insensitive at the filesystem, so it costs them nothing today and I cannot run their tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every project this editor has ever made had a module called
BlankGamein it.NewProjectDialoghandedCopyModulethe same id for source and target, so the engine's rename path never ran: the game module kept the template's name, an emptyAuthor, and the template's description — "A blank game, ready for you to craft into something amazing!" — no matter what was typed into the dialog.Fixing that turned up two deeper bugs, one of them a data bug that has affected every project ever created on Linux. Three commits, in the order they were found.
1. A new project that is named after your game
The dialog asks for six things now — Title, Directory, Game Core, Module Name, Author and Description — each with a tooltip on the caption as well as the input.
Game(Space Rangers→SpaceRangersGame) and follows the title until you type your own, after which it's yours.Renaming a module is not renaming its
ModuleId. The engine calls<ModuleId>::<CreateFunction>, so the id inmodule.tamland the namespace in the module's script are the same name written twice, and asset ids are that name a third time —<ModuleId>:<assetName>, in script and in every taml that references an asset. Rewrite onlymodule.tamland the module still loads, still reports its new name everywhere the UI looks, and silently does nothing.So the rename is a pass over the module's own source, in one place —
ModuleStamper— called from all three paths that rename a module. The other two were worse than the New Project one because they can be reached repeatedly: New Module from a template and Edit Module changing a name both rewroteModuleIDand stopped there. The substitution is whole-word, because in the Edit Module case the old id is whatever someone called their module and can be a substring of an ordinary word.The engine has half of this already and it isn't enough:
copyModule'sTamlModuleIdUpdateVisitoris root-only so a nested asset id is missed, it can't touch.csat all, and it renamesmodule.tamlto<ModuleId>.module.taml— a name nothing else in the editor opens a definition by, with nofileRenamebound to script to put it back.Templateis now a flag andTypesays what a module is.Type == "Template"was only ever read as an enumeration filter; "leave this module alone" is enforced by the engine'sSynchronizedfield and always was. So BlankGame isType="Game Core", ArtPack isType="Art Pack", both carryingTemplate="1"and aDisplayName.2. A declared path spelled the way it was written
A project's game module declared
Path="Sprites"andPath="Fonts"where the template saidspritesandfonts. On Windows nobody notices. On Linux those directories don't exist, so an image dropped intosprites/afterwards is never scanned and never becomes an asset — silently, with themodule.tamlsitting there looking correct. AppCore got it too, so this affected every project this editor has made.Nothing in the path code was wrong. The fields were
TypeString, which interns withoutcaseSens, andStringTable's hash is case-insensitive by construction —hashStringruns every byte through a to-lower table, so two spellings differing only in case always land in the same bucket, and an insensitive insert returns the first one there rather than the one it was asked for.TypeCaseStringalready existed and already does the right thing, so this is a field-type change rather than new machinery.Deliberately left folded:
ModuleId,Group,Typeand the create/destroy function names. Those are identifiers, they are compared as string-table pointers —ModuleManagerdoes it in nine places for load order, groups and dependency resolution — and that matching has always been case-insensitive, so a project depending onAppCore=1resolves against a module spelling itselfappCoretoday. A test assertsModuleIdstill folds, so it reads as a decision.3. The folder a scan names is the folder you can open
BlankGamedeclared eight asset paths and shipped three, so every new project warned four times on first open about directories the template never had. Adding them — each with a readme saying what belongs there — is the content change. Adding them turned up the rest.A directory called
fontscame back out ofreaddiras"Fonts". Not sometimes: always, on this platform. Two of the colliding spellings are interned during static initialisation, before any engine code runs —SpriteBatchinterns"Sprites"as a taml node name andguiProfileThemeinterns"Fonts"as a field group. Sospritesandfonts, the two most ordinary names an asset folder can have, were the two that could never survive a scan. That's whydirectoryDeletecouldn't recurse into a folder it had just been told about.The platform layer now interns what
readdirgives it case-sensitively — and so doResManager::getPaths, the zip andopenFileForWritepaths, andResDictionary's own path normalisation. Those had to move together: the dictionary derives its bucket from the pointer values of path and name and compares by pointer, so a half-applied change doesn't merely compare false, it hashes into the wrong bucket and reports an existing file as missing.The four editor themes are part of this rather than incidental. Each asked for
^EditorCore/Themes/<name>/Fontswhile the directory on disk isfonts, and that's where$GUI::fontCacheDirectorycomes from. It worked only because the fold rescued it —guiProfiles.csasks for the same LabCoat directory in lower case two files away, which is the codebase disagreeing with itself. Left alone they'd have become a real missing-font-cache bug the moment lookups stopped folding.Testing
322 unit tests, 0 failures. 50 of 50 TorqueScript smoke suites.
New coverage:
stringTableCaseTestsdeclaredPathCaseTestssetDataField, the route TAML takes. Carries a control:ModuleIdis poisoned and set identically and still folds, which is what makes the six passing cases evidence about the field type rather than about the fixturedirectoryScanCaseTestssprites/readme.mdandfonts/readme.md, plants the capitalised spellings by hand so the collision can't be absent by luck, and finishes by deleting the tree the scan just enumerated — the exact step that failedtests/smoke/newProject.cstests/smoke/moduleRename.csBoth smoke suites end by loading the renamed module and asserting its gui reached the Canvas, because that is the only thing that proves
createactually fired.Also verified by hand: two projects built through the real UI, both fully renamed,
grep -ril blankgameempty across the tree.Notes for review
x86UNIXFileio.cc,resourceManager.ccandresourceDictionary.ccare CRLF; the diffs preserve that..csfiles come out CRLF, becauseFileObject::writeLinealways emits\r\n. Harmless for the lexer and TinyXML, but it shows in a diff.🤖 Generated with Claude Code