Conversation
… are not built or installed
Add TextTransform class to evaluate the text params in one place __RequiresTextTransform and __TransformTextLines both read the same params and repeated the same conditions for them, so a new param had to be added twice and the two lists could silently disagree. TextTransform evaluates each param exactly once on construction and answers both questions, whether a transformation is required and what it does to the text. The text reading, writing and filtering that only exists for these params moves along with it, which leaves BuildCopy with a single __WriteTextFile helper for the single source and multi source cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tool was distributed as a PyInstaller onedir freeze, archived with a bundled 7z.exe and downloaded by consumers via batch scripts that pinned a hardcoded size and sha256. Building it required three virtual environments driven by a data-driven buildproject.py, plus Poetry. None of that is needed to ship a Python package. Remove it wholesale: - ModBuilder/generalsmodbuilder -> generalsmodbuilder (repo root), so the repo works directly as a git submodule, a plain file copy, or a `pip install git+...` target with no subdirectory indirection. - Delete buildproject.py, build.json, build-lite.json, the windows/*.bat helpers and the bundled 7z.exe / 7z.dll. - Delete poetry.toml, poetry.lock and poetry-instructions.txt. - Add __init__.py to the changelog and config directories so every importable directory is a real package. - Add a root .gitignore. The build/ and dist/ rules are anchored to the repo root so they cannot match the generalsmodbuilder/build package. - Repath the vscode configs and drop the now dead "ModBuilder - Build" launch configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the Poetry metadata with a standard [project] table so the repo can be installed by any PEP 517 frontend, and declare a console script entry point so `generalsmodbuilder` is a real command after install. Drop scikit-image. It was declared as a dependency but is imported nowhere in the source; it alone pulled in scipy, PyWavelets, imageio, tifffile and networkx, which is why the old PyInstaller step needed postDeleteFiles rules to strip skimage sample data and scipy.libs back out of the dist. numpy and Pillow still arrive transitively through psd-tools, the only real consumer. The resolved dependency set drops from ~30 packages to 13. Raise requires-python to >=3.12 and unpin the remaining dependencies from their 2022-era versions. Nothing on the consumer machine constrains this anymore because uv downloads a suitable CPython itself. Make generalsmodbuilder/__version__.py the single source of truth for the version, read by [tool.hatch.version], instead of duplicating it here and there. Bump to 3.0, since the distribution contract changes completely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
util.GetAbsSmartFileDir existed solely to redirect package-relative paths to the PyInstaller executable directory. With no frozen build left it is dead weight, so drop it along with g_isFrozen and resolve g_appDir directly from the package location. All seven call sites move to the existing GetAbsFileDir, which is what they already did when running from source. Point the built-in tools configuration at a per-user cache directory when --load-default-tools is used without --tools-root-dir. It previously resolved relative to the json file itself, which is now inside the installed package; a pip installed copy would try to download blender into site-packages. Consumer tools configurations are unaffected, since those still resolve against their own directory. Add __main__.py so `python -m generalsmodbuilder` works alongside the console script, and drop the leftover PyInstaller pkg_resources workaround comment in main.py. Also fix ToolFile.VerifyInstall comparing self.md5 against the computed sha256, which made sha256 verification of downloaded tools always fail on that path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
modbuilder.cmd is the entire consumer-facing install story. It installs uv if the machine does not have it, and uv then downloads a suitable CPython and the locked dependency set into a virtual environment beside the script before running the tool. This replaces the previous flow, where a consumer project shipped its own 7z.exe and ~120 lines of batch to curl a release archive, check a hardcoded size and sha256, and extract it. modbuilder.sh is the equivalent for POSIX shells. `uv run` is deliberately used without --locked so that pulling a submodule update with a changed lock file heals itself rather than failing. CI uses --locked instead to catch drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
modbuilder.sh is checked out on Windows too, where the repo default of text=auto would give it CRLF endings that sh cannot parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Runs on Windows and Linux: syncs the locked dependency set, exercises both entry points, imports every module, builds the wheel and asserts the wheel carries the built-in configurations and the gui icon. The import step is what earns its keep. It catches a dependency update breaking beeprint, psd-tools or markdownmaker, and catches a Python built without tkinter, which the gui needs. A real mod build cannot run in CI, because crunch, gametextcompiler and blender are Windows binaries that are downloaded at runtime. uv sync uses --locked here, unlike the launcher, so that a lock file which has drifted from pyproject.toml fails the build instead of being silently regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the release-archive install instructions with the three supported ways to consume the tool: git submodule, plain copy, or an installed Python package. Add a short developer and release section, since the repo previously had no written build or release procedure at all. Reword the safety section. The sha256 pinning of the downloaded game tools is unchanged and still applies, but the Mod Builder itself is no longer a downloaded archive, so it has no archive hash to pin. Its integrity now comes from the submodule commit or git tag, and its dependency hashes from uv.lock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The duplicate target file check in BundleItem compared raw strings, so an item declaring Data/INI/Weapon.ini and data/ini/weapon.ini passed verification and then silently overwrote one target with the other during the copy, whichever came last. File names are case insensitive on Windows and in big archives, so both spellings name the same file. The same held for the duplicate item name check. Add util.VerifyUniqueNames as the single place that decides whether two names collide, and route every name uniqueness check through it. It distinguishes an exact duplicate from a collision that differs in upper and lower case only, so the error says which of the two problems it found. Several collisions were not checked at all and are now rejected at config load time, before any build step runs: - two items building a big file of the same name, which distinct item names can still produce through namePrefix and nameSuffix, - two packs with the same name, which had no uniqueness check whatsoever, or building a release .zip file of the same name, - a pack listing the same item more than once, which built that item into the pack twice, - two items of the same pack building a file of the same name into it. The last one may reject configurations that build today, where a later item in a pack overwrites a file of an earlier one. That is not a documented overlay mechanism and the build treats it as ambiguous, since the diff registry keys targets by lowercased path and keeps only one entry per target. Move the big file and release file naming into BundleItem.GetBigFileName, BundleItem.GetPackTargetFileNames and BundlePack.GetReleaseFileName, and call those from the engine, so that the names the verification checks cannot drift from the names the build produces. Tool names in tools.py are deliberately left alone. Their dict keys are case sensitive while every lookup uses a lowercase literal, so a tool named Crunch becomes an unreachable second entry rather than an override, but changing the key semantics would change how configurations layer on top of DefaultTools.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A missing build tool was only noticed at the moment it was used, deep inside the Build step and, with multi processing enabled, inside a pool worker. A tool that was defined but whose executable was absent was not checked at all and failed with a bare FileNotFoundError from the process call. The Build step now collects the tools that its build files require and verifies them before it copies the first file. A tool is missing when it is not defined or disabled, or when its runnable executable does not exist. The error names the tool and the first target file that requires it. The required tool is derived from the same dispatch tables that the copy itself uses. Copy functions that call a tool are marked with a RequiresTool decorator, and BuildCopy.GetRequiredToolName resolves a copy without performing it, so there is no second table that can drift out of sync. To keep that answer exact, the decision whether a DDS to DDS copy needs crunch moved from inside __CopyToDDS up into __GetCopyFunction. Without params such a copy is a plain file copy and requires no tool, which is unchanged behaviour. BuildCopy.__GetToolExePath is left as a plain dictionary lookup, because it runs once per copied file and in every pool worker. The four blanket tool warnings in BuildSetup.VerifyValues are removed. They warned regardless of whether the bundles contained such files and are superseded by the precise check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Linux CI job failed with ModuleNotFoundError: No module named 'winreg'. util.py imported winreg unconditionally, and nearly every module imports util, so the whole package was unimportable outside Windows. Guard the import and set winreg to None when it is absent. GetRegKeyValue then returns None and SetRegKeyValue returns False, which is what both already do when a key is missing, so callers need no change. The HKEY_LOCAL_MACHINE default arguments were evaluated at import time and had to move into the function bodies behind a None default. Every caller uses the default root, so behaviour on Windows is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VerifyType built its message with expectedType.__name__, which only a plain type carries. When the expected type was a tuple of types or a union, the message raised AttributeError instead of the AssertionError that was meant to describe the mismatch. Both shapes are in use: VerifyParamsType passes a tuple, and the changelog parser passes str | date | datetime. Name the expected type through GetTypeName, which handles a plain type, a tuple of types and a union, so a mismatch is always reported as intended. Add pytest and wire it into the CI workflow, so that the checks added by the following json parser changes can be verified as they land. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parsers read raw json with dict.get and let a later VerifyTypes pass
report a bad value. That pass can only name the python field the value ended
up in, so a user is told that "Folders.absReleaseDir" is not a string without
being told that the key to look at is folders.releaseDir. Values also reach
path joins and string methods before that pass runs, where a wrong type
raises a bare TypeError instead of a described failure.
JsonContext names the place a value is read from and is built up while
descending into the data, so a failure reads
ModBundleItems.json: bundles.items[3] 'GameFiles'.files[2].target
is type:int but should be type:str
GetOptional and GetMandatory distinguish a key the format allows to be
absent from one it requires, and both verify the value where it is read.
The optional elementType verifies the members of a list at the same time,
which is what most of the list valued keys need.
No parser uses it yet. The parsers move over to it in a following change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ToolCallInstruction defaulted callArgs to the ParamsT type instead of an instance of it, so a callList entry that did not set callArgs made the verification call items() on a type and raise AttributeError. Replacing aliases in a callArgs dict called replace() on every value, but a call argument may also be a number, a bool or a list, which raised AttributeError. It also wrote the results back into the parsed json data. Only string values are rewritten now, into a new dict. The http error message was missing its f prefix and printed the placeholder instead of the code. Tool.Install raised on failure, so the flag it returned was always True and the branch that tested it in InstallTools could never be taken. Both now return nothing. BuildFiles.Normalize rebound the loop variable and wrote nothing back, so build file paths were never normalized. AddBuildFilesFromJsons was annotated to return Bundles while returning nothing, and BundleItem.ResolveWildcards was annotated to return nothing while returning a list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…joining MakeToolsFromJsons assigned the directory of the first json that carried a tools section to its rootDir parameter. Every later tools json then resolved its targets against that first directory, although the tools-root-dir help states that a tools json roots its tools in its own directory. An explicit override still applies to all of them. Aliases were replaced after the target had been joined to the root directory, so an alias standing for an absolute path was appended to the root instead of replacing it. They are replaced before the join now. The blender addon install argument in DefaultTools.json named THIS_DIR, which is the directory of the packaged configuration. Since load-default-tools began installing into the per user cache directory, the zip is downloaded there while the argument still pointed inside the installed package, so the addon was installed from a file that does not exist. It names ROOT_DIR now, which is where the zip is actually downloaded to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both parsers read their keys with dict.get and left every type to a later VerifyTypes pass. A wrong value was reported by the python field it ended up in, so a user was told that Folders.absReleaseDir is not a string rather than that the key to look at is folders.releaseDir, and a value could reach a path join before the pass ran. Their keys are read through JsonContext now, typed and marked optional where they are read. VerifyTypes keeps only what a read cannot see, which for folders is that the merged result has both directories, since either may be set by any of the configuration files. ParsedData and FinalizeParsedData capture the phase order that all six parsers follow, so the sequence is stated once instead of being repeated by hand, and the class documents which checks belong in which phase. The remaining parsers move over to it in the following changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runner keys are read through JsonContext now, typed and marked optional where they are read, so a wrong value names the file and the key instead of the python field. VerifyTypes keeps only the check that a read cannot express, which is the types of the values inside the arguments dict. The four registry keys were read by four near identical blocks. They share one helper now, which also guards against a registry value that is a number rather than a path, since a value read from the registry may be either and joining a number as a path raises TypeError. The regular game data files were normalized before they were joined to the installation directory, so the paths that are actually used were never normalized. They are joined before the phases run instead. Runner.ResolveWildcardsInFileList is replaced by util.ResolveFileWildcards. The two differed in one way that matters: the runner list says which game data files are allowed to be present, not which ones are required, so it must neither verify that a listed file exists nor report a wildcard that matches nothing. Almost every entry belongs to a language that is not installed. ResolveFileWildcards states that difference as filesMustExist. VerifyStringListType had no caller left and is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tools keys are read through JsonContext now, typed and marked optional or mandatory where they are read, so a wrong value names the json file, the tool and the file index instead of the python field it ended up in. Reading the format version as an int also settles a case that could not be reported at all: the version is compared against 1 to tell the two tool version layouts apart, and comparing a string against a number raised TypeError before any check ran. VerifyTypes keeps only what a read cannot see, which is the types of the values inside a callArgs dict. The messages of the remaining value checks name the json keys rather than the python fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
changeconfig was the one parser that already checked values where it read them, but its messages named the bare key, without the file, the record or the element, and the file lists were joined into paths without checking that their entries are strings, where a number raised a bare TypeError. Its keys are read through JsonContext now, so a record is named by its index and a bad list entry by its position. sourceList and targetList are read as mandatory, which is what they already were in effect, since a record without them failed later with a message about a python field being None. VerifyTypes is gone from all three classes. Every type it checked is now either guaranteed where the value is read or is state this module builds itself. The checked getters in util stay, because the yaml changelog parser uses them and is not part of this work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bundles keys are read through JsonContext now, typed and marked optional or mandatory where they are read, so a bad value names the json file, the item or pack it belongs to and the element index inside it. A missing source in a sourceTargetList entry, for example, used to surface as an absSourceFiles value being None, several steps away from the key at fault. VerifyTypes is reduced to what a read cannot see, which is the types of the values inside a params dict. The messages of the value checks name json keys rather than python fields. The registry list was joined into absolute paths inside the parsed json data, which is the caller's data. A new list is built instead. BundleRegistryDefinition keeps doing its work in its constructor rather than through the shared phases, because one definition is shared by every file of the entry that declares it and a wildcard entry expands into very many of those, so running the phases per file would repeat the same work. Its messages named a class that does not exist. The json name of all seventeen event types was rebuilt for every item and every pack. The lookup is built once at module level. Verified against both real projects: the sample project and the game patch parse unchanged, and the four parser level negative fixtures of the sample project still fail with their original messages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sumes A files entry that named none of source, sourceList, sourceTargetList, multiSource or multiSourceTargetList contributed no file at all and said nothing about it, so a misspelled source key was invisible. An empty sourceList, sourceTargetList or multiSourceTargetList did the same, although an empty multiSource was already rejected. Both are rejected now, so the rule that multiSource already followed holds for all five keys. These checks run after the check that source and multiSource are not named together, so an entry naming both keeps being reported as that rather than as something else. A target given next to a sourceList or a sourceTargetList alone was silently ignored, because those derive their targets from their own source files. It is rejected unless the entry also has a source or a multiSource to consume it, which keeps the multiSource with sourceTargetList combination valid that the sample project uses. The multiSource messages name the file, the item and the entry now, like the other messages do. That params and registryDef are shared by every file an entry builds is stated where they are declared, since nothing enforces it and the build relies on them not being modified after parsing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When no game installation directory was found, the directory stayed empty and Normalize turned it into the current directory, which passes isdir. The failure then blamed the game executable for not existing at a path that had never been searched, and the regular game data files silently resolved against whatever directory the build was started from. The same happened when gameExeFile was absent, because the search is driven by that file name and was skipped entirely without it. An empty path is left empty by Normalize now, gameExeFile is required, and a missing installation is reported as such, naming the executable that was looked for, the four keys that can supply a directory, and every directory that was actually searched. folders rejects a releaseDir that names the same directory as buildDir, since the release is built out of the build directory and would overwrite it. The two are compared the way file names are compared on Windows, where paths that differ in upper and lower case only name the same directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tool.version held the numeric version of a version 1 tools json and was read by nothing, while the two log lines only ever printed versionStr, which stays empty under version 1. A version 1 tool was therefore logged with no version at all, and the float check rejected an integer version. The field is gone and a numeric version is kept as a string, so both layouts log the same way. A callList entry without a call was parsed, kept and then silently skipped when the tool installs, so a mistyped key meant the call never happened. The call is required now, which also removes the guards that every use of it needed. A tool may mark only one file as runnable. Only the first one can ever be the executable, so any further one was accepted and then ignored. A file that has no url and whose target is missing or does not match its size or hash ends as NoInstall, which matched none of the message branches, so the failure said only that the file was not installed. It says why now. This is the reachable failure for the extracted blender executable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An unrecognized sort direction became Sort.Zero, which the changelog generator compares against neither Ascending nor Descending, so it left both its comparison results false and the entries were treated as equal. A typo such as "ascendign" therefore dropped that sort rule without a word. The direction must now be one of the two, named in the failure. A sortList entry naming neither date nor label was skipped just as quietly. An entry naming both was accepted and its label ignored, because only the date was read. An entry must name exactly one of them now. Sort.Zero was only ever produced by the case that is now rejected and was consumed by nothing, so it is removed. SortDefinition.VerifyValues guarded against a definition that is neither kind, which the parser could not build before and cannot build now, so it goes with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build sections were followed exactly one level. A json file pulled in through build.files could declare its own build section, which nothing ever read, so the files it listed were silently left out of the build. They are followed to any depth now. Each file is read once, identified by the file it actually resolves to. Listing the same file twice, reaching it through a diamond of includes, or naming it by two different paths used to read it twice, which failed much later with a bundle item name being used more than once. A cycle terminates instead of recursing forever. The files that were given on the command line keep their order and come first, as before, because a later configuration file overrides an earlier one and the bundle prefixes carry over from one file to the next. An entry that is empty, and an entry naming a file that is not json, are rejected. Both used to pass the parser and then be dropped without a word, the first by a guard against an unusable path and the second by the extension filter that decides which files are read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key that the format does not define was ignored in silence by all six parsers, so a misspelled key behaved exactly as if it had never been written. A mistyped onPreBuild meant the script never ran, a mistyped sourceList meant the files were never built, and nothing said so. Every section now declares the keys it defines and rejects anything else, naming the file, the section and the key. The info key of a tool is declared even though nothing consumes it, because it describes the tool for whoever reads the configuration. Only tools read its version. The other five sections declared one that nobody looked at. Each section reads and verifies its version now, so a configuration written for a newer format is rejected rather than parsed as if it were the current one. All current data is version 1, so there is one layout to parse, but the version is where a future breaking change is handled, which is what it exists for. SETTINGS.md states both rules, documents the tools section, which was never listed, and corrects sourceTargetList[].target, which the table called mandatory while the code has always defaulted it to the source file. Verified against every json file of the sample project and the game patch: all of them parse, and every key they use is a known key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The multi source table described a str target built from str sources twice, once as appended and once as merged, and left the reader to guess which one happens. It now says that a str target is appended while every source is a str file and is merged as soon as one of them is a csf file, which is what the builder does. The params of a file entry were only documented by pointing at the sample project. They are listed now, with the values they take, the file types they apply to and the defaults, since the configuration is verified against exactly that list. The behaviour that the audit settled is written down with them: line endings are kept unless forceEOL names one, an exclusion marker region is counted over all source files of a target together, the w3d exporter has no mode for two of the eight flag combinations, and a dds file is only compressed again for a param that asks for texture work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The known param names had to be written in lower case, because the lookup lowers the key it is given. Nothing enforced that, so a name declared in any other case would simply never match and the param would go unverified in silence. Lower the names once, where the tables are merged, and write each of them the way the mod projects and the build step spell it. Write the w3d names out one per line like the other tables instead of running a comprehension over them. The merged table is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The game install directory, executable and arguments could only come from the runner section of the configuration files, which a mod project commits to its repository. A user whose game sits where the registry keys do not name had to edit a tracked project file. JsonRunner holds what the configuration files say, UserRunner holds what one user set for their own machine, and MakeRunner merges them into the Runner the engine consumes. A user install directory is the only candidate searched, so it is never passed over in silence, and a user argument list replaces the configured one entirely. Runner.gameExeArgs is now the finished argument list, so the engine passes it to the process without converting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…guments --game-install-path, --game-exe-file and --game-exe-args build a UserRunner that wins over the runner configuration, so a game that sits where the registry keys do not name is reached without editing a project file. SplitGameExeArgs turns the one string a user writes into the argument list. Posix splitting is avoided because it eats the backslashes of an unquoted windows path. The argument parser moves into MakeArgumentParser so the mapping from arguments to a UserRunner can be tested on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gui needs somewhere to keep what the user set, and it must not be next to the project, where it would be committed to the mod repository. UserSettings.json holds a userRunner section under the platformdirs user config directory, beside the tools cache that the default tools configuration already uses. The section carries a version and rejects an unknown key like every other configuration section, so a hand edit is reported the same way. A file that cannot be read is reported and leaves the settings that change nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fields below the existing frames set the game install path, the executable and its arguments, with a Browse button for the directory. They are seeded from the command line where it gave a setting and from the saved user settings otherwise, and they are written back when a job starts and when the window closes. An empty field means no setting, so the runner configuration is used. Every action passes the fields on, so Install, Uninstall and Run all use the same game directory. JoinGameExeArgs writes an argument list back as the command line it came from, which is what fills the arguments field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seven build operations were spelled out five times over: a BooleanVar field, a checkbox, a button, an entry in _SetJobElementsState and a run method that differed from its neighbours in one keyword argument. They are now one Operation table that both columns and the state handling walk, and the seven run methods collapse into _RunOperation. The arguments of a job stay lazily evaluated, because the bundle pack selection and the engine are made by _OnWorkBegin after the button is pressed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gui used whatever ttk theme the platform provided. It now carries a dark palette of five colours, registered with ttkbootstrap, which builds its widget art from a colour dict and so can take colours that are ours. The palette is only legible on a dark background, which the tests assert by contrast ratio, so there is deliberately no light mode. The window grows a little to fit the larger font. Its layout is unchanged here and is rebuilt in the next step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every group was a solid relief frame with a label floating above it, which drew a notch where the empty label of a LabelFrame goes and left the boxes of one row starting at different heights. There is now one Section helper that every box is built from, so the boxes line up by construction. The columns are reordered, the game launch settings move above them, Execute sits with the sequence it runs, and a status bar carries the state, a progress bar and Abort. The window is resizable and asks Windows for per monitor dpi awareness, which it needs to stop the text blurring on a scaled display. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list box was the one widget the theme could not reach, so it kept the system colours and its selection stayed the system blue. A tree view carries the theme, and a check mark drawn with Pillow reads at the row height where a font glyph did not. A click anywhere on a row still toggles it, as the multiple selectmode of the list box did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window was a control panel in front of a console the user had to keep watching. It now carries the output itself, on a sash so it can be dragged larger or shut. Standard output and standard error are teed rather than replaced, so the console still receives everything and modbuilder.cmd is unaffected. Note that the external tools are started with subprocess and inherit the console handle, so their own output reaches the console but not the pane. Widgets are no longer touched from the build threads. Work is posted to a queue that the main thread drains on a timer, which also folds the abort polling loop into the pump and removes a thread. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Abort is greyed for most of a session, because the engine only holds a process handle while the game runs. A build tool is started and waited on without being recorded, so there is nothing to terminate, and the greyed button looks broken rather than inapplicable. A hint on the button says so. It is shown from a small tooltip helper, since this version of ttkbootstrap carries none, and a disabled widget still reports the pointer, which is the case that needed explaining. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <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.
Uh oh!
There was an error while loading. Please reload this page.