Skip to content

Apeiron 0.1.0: independent Minecraft launcher (Java Edition, C11 log core, bilingual UI) - #1

Merged
hekuo5310 merged 20 commits into
mainfrom
feature/apeiron-launcher
Oct 28, 2026
Merged

hekuo5310 merged 20 commits into
mainfrom
feature/apeiron-launcher

Conversation

@hekuo5310

Copy link
Copy Markdown
Member

Apeiron 0.1.0 — full launcher implementation

Implements the launcher described in the task brief: a Chinese-first, bilingual
desktop launcher for Minecraft: Java Edition on Windows, macOS and Linux, with a
C11 logging core compiled into the application.

What is included

Native logging core (csrc/, apeiron._logcore) — bounded queue, dedicated
writer thread, size-based rotation with retention, console + file sinks,
millisecond timestamps, PID/TID, exception context, UTF-8 validation and
credential redaction. Portable across pthreads and Win32. Standalone C harness
(44 assertions) plus Python integration tests.

Java Edition pipeline — official manifest with cache; version inheritance;
deny-by-default rule evaluation; Maven-base library resolution; client,
libraries, natives and assets with SHA-1 verification; Java discovery by
execution (never by assumption) with optional Temurin provisioning; per-version
argument construction for both structured and legacy formats; process
supervision with pipe draining and log capture.

Accounts — Microsoft OAuth 2.0 device-code flow through Xbox Live and
Minecraft services with an explicit ownership check; tokens stored only in the
OS credential store (DPAPI / Keychain / libsecret) with a flagged file fallback;
offline profiles using the vanilla offline-UUID algorithm, always presented as
unverified.

Content — Fabric and Quilt from official metadata; Forge/NeoForge via the
vendor installer after SHA-1 verification and with explicit consent; Modrinth
.mrpack import/export; CurseForge import gated on the user's own API key;
mods, resource packs, shaders and data packs.

Instances — create, copy, rename, delete with path guards, world
backup/restore, archive export/import that omits credentials and caches.

UI — eight-page Material-style QML interface, light/dark/system themes,
keyboard shortcuts, accessibility names, 179 translated strings, Chinese default
with persistent manual override.

Security — safe archive extraction (traversal, absolute paths, symlinks,
size bombs), HTTPS-only transport, atomic verified downloads, log redaction,
instance-root guards.

Docs, licences, CI — README, authoritative English disclaimer plus a
Chinese reference translation, contributing/security/changelog, architecture,
platform, account, build, test, i18n, licence and disclaimer-policy documents;
CI for lint/types/C/Python/i18n/QML; Nuitka build workflow for three platforms.

Verification performed

Check Result
pytest -q see docs/TEST_REPORT.md
Standalone C harness see docs/TEST_REPORT.md
ruff check / ruff format --check clean
mypy src/apeiron clean (Qt bridge excluded, documented)
Headless UI smoke (33 checks) see docs/TEST_REPORT.md
Real Minecraft install + launch see docs/TEST_REPORT.md
Nuitka artifact + --self-check see docs/TEST_REPORT.md

docs/TEST_REPORT.md is the source of truth and records the environment, the
exact commands, what passed, the failures found and fixed during development,
and — importantly — what could not be verified here.

Not verified here (stated, not hidden)

  • Microsoft sign-in end to end: requires an Azure AD client id owned by the
    publisher and a licensed test account. The flow is implemented against the
    official endpoints and fails with an actionable error when no client id is
    configured.
  • Windows and macOS builds: no runner of those platforms was available in this
    environment; the Nuitka configuration is present and the Linux artifact was
    built and self-checked.
  • Bedrock activation: requires Windows with a Store-licensed installation.
  • NeoForge installs: its Maven host was unreachable from the build network.

Compatibility and risk

  • Python 3.10–3.12, PySide6 6.9.x (Essentials + Addons, the latter for the
    Material elevation effect).
  • Update manifests are checksum-verified over TLS but not cryptographically
    signed yet; documented in SECURITY.md.
  • Apeiron does not redistribute Minecraft files and does not bypass licensing.

Apeiron Development added 10 commits October 23, 2026 22:34
The launcher must never block the Qt event loop on disk I/O and must never
write credentials to disk. Both requirements live in C rather than Python:

* bounded ring queue (4096 records) with a dedicated writer thread, so a
  caller never waits on the filesystem;
* size-based rotation with retention, console + file sinks, ISO timestamps
  with milliseconds, PID/TID and exception context;
* UTF-8 validation with explicit rejection of malformed input instead of
  writing mojibake;
* redaction of keyword values (access_token, refresh_token, password,
  authorization, ...), Bearer credentials and JWT-shaped runs, applied
  before a record is queued;
* portable threading layer (pthreads / Win32) so the same source builds with
  gcc, clang and MSVC;
* standalone test harness (44 assertions) that runs without Python, plus the
  CPython bindings kept in a separate translation unit.

The extension is compiled by setup.py and shipped inside the Nuitka artifact;
it is not a separate service or shared library release.
Foundation shared by every feature:

* AppPaths keeps every artefact under one per-user root, validates instance
  ids against traversal and creates directories with 0700 on POSIX.
* Settings are validated and clamped on load, written atomically (fsync +
  os.replace), migrated from the v1 schema, and unknown keys from a newer
  build are preserved instead of dropped.
* HttpClient is HTTPS-only (plain HTTP is allowed solely for loopback in
  tests), requires TLS 1.2+, refuses redirect downgrades, retries idempotent
  failures with jittered backoff and honours Retry-After.
* SQLite uses WAL, foreign keys and append-only migrations recorded in
  PRAGMA user_version; repositories hold SQL only so business rules stay
  testable without a database.
* DownloadManager provides bounded concurrency, per-attempt timeouts,
  cancellation within one chunk, .part files with fsync + atomic replace,
  SHA-1 verification, disk-space and writability pre-flight checks, duplicate
  suppression and structured progress dataclasses instead of log scraping.
…nces

Java Edition
* Version metadata is parsed faithfully: OS/arch/feature rule evaluation with
  the vanilla deny-by-default semantics, inheritsFrom merging, structured and
  legacy argument formats, and Maven-base library entries that mod loaders
  publish instead of resolved artefacts.
* JavaInstaller is idempotent and content-addressed (libraries, natives with
  extract filters, client jar, asset index and objects, log4j config), so an
  interrupted installation resumes by rerunning it.
* Java runtimes are never assumed: every candidate is executed with -version
  and parsed; a suitable Temurin build can be installed from the official
  Adoptium API with SHA-256 verification.
* build_command() is a pure function producing per-version arguments, exactly
  one -cp pair, validated memory flags and a token-redacted launch log.
* GameProcess drains both pipes on a thread, mirrors output to a per-launch
  log and reports a structured exit status.

Accounts
* Microsoft sign-in uses the official OAuth 2.0 device-code flow through Xbox
  Live and Minecraft services, performs an explicit ownership check and stores
  tokens only in the OS credential store (DPAPI / Keychain / libsecret), with a
  0600 file fallback that the UI marks as insecure. No client id is shipped.
* Offline profiles use the vanilla offline-UUID algorithm (verified against
  OpenJDK) and are always presented as unverified local identities.

Content
* Fabric and Quilt install from official metadata profiles.
* Forge and NeoForge run the vendor installer only after its published SHA-1
  matches, and only with explicit consent.
* Modrinth .mrpack import/export; CurseForge import gated on the user's own API
  key; mods, resource packs, shaders and data packs with enable/disable.

Instances and Bedrock
* Create/copy/rename/delete with path guards, world backup/restore and archive
  export/import that omits credentials and caches.
* Windows Bedrock adapter detects Store packages and activates them through
  shell:appsFolder; explicitly limited, no file modification or licence bypass.
* Eight pages (home, library, accounts, downloads, content, settings, logs,
  about) built on Qt Quick Controls' Material style. Qt implements Material 2,
  so the Material 3 semantic roles used by the design are provided explicitly
  by Apeiron/Components/Theme.qml instead of referencing properties that do not
  exist; the gap is documented rather than papered over.
* Components live in one URI module (Apeiron.Components): importing the same
  directory through several relative paths produced duplicate type registries
  and made composite types fail to instantiate.
* The backend is a context property with C++ ownership, a C++ parent and a
  strong reference: a Python-owned QObject exposed to QML was being collected,
  which surfaced as null-property errors in every binding.
* Long work runs on a thread pool and reports through structured signals; the
  pool is drained before the engine is destroyed.
* Language changes rebuild the component tree (cached qsTr bindings are not
  reliably re-evaluated by retranslate); UI state lives in the backend so the
  selection survives.
* 179 strings translated to Simplified Chinese via Qt Linguist .ts/.qm, with a
  build script that fails when a qsTr string has no translation.
* Keyboard shortcuts (Ctrl+1..8, Ctrl+L, Ctrl+Q), accessibility names and
  Chinese-capable font fallbacks.

tools/gui_smoke.py exercises the real interface headlessly and fails on any Qt
warning; tools/e2e_minecraft.py installs and launches the actual game.
169 pytest cases plus a 44-assertion standalone C harness cover the behaviour
that is easy to get subtly wrong:

* native logging: state errors, filtering, UTF-8 round-trip, rotation,
  concurrent writers, redaction before disk, shutdown idempotency;
* downloader: success, duplicate skip, checksum mismatch, retry, 404, timeout,
  cancellation, disk space, unwritable target, concurrency, atomic replace;
* version metadata: deny-by-default rules, inheritance, argument expansion,
  Maven-base libraries, classpath de-duplication;
* launch: modern and legacy command construction, exactly one classpath pair,
  token redaction, process I/O and termination;
* accounts: offline UUID equals the OpenJDK value, rename semantics, no secrets
  in SQLite, client-id requirement, device-code payload redaction;
* security: zip/tar traversal, absolute paths, symlinks, size bombs, URL policy,
  update checksums, log redaction;
* instances and modpacks: isolation, delete guards, backup/restore, export and
  import round-trip, unsafe pack paths, CurseForge key gating;
* i18n and Bedrock: locale fallback chain, manual override, .qm loading, Appx
  parsing and unsupported-platform errors.

GUI coverage runs the real QML interface in its own process (Qt teardown inside
a shared pytest process is not deterministic) and asserts window creation, all
eight pages, language switching in both directions, account and instance
lifecycle, theme persistence, navigation and zero Qt warnings.
* README states the product position, supported platforms and versions, install
  and build steps, account modes, offline limitations, disclaimer and how to
  report a bug.
* DISCLAIMER.md is the authoritative English statement; DISCLAIMER.zh-CN.md is
  marked in its header as a reference translation without independent legal
  effect, and docs/DISCLAIMER_POLICY.md records how the two relate.
* docs/ documents architecture (including the Qt lifetime rules that cost real
  debugging time), the verified support matrix, account flows, building with
  Nuitka, the test layers, third-party licences and the i18n workflow.
* CI checks formatting, lint, types, the C harness, the Python suite, i18n
  completeness, a headless QML load, and workflow syntax, with read-only
  permissions. The build workflow produces Nuitka artifacts for the three
  platforms and fails unless the packaged binary passes --self-check.
The negation re-included the compiled extension in the index; build artifacts
must stay out of the repository.
The Adoptium CDN is throttled on some networks; refusing to fall back to a
local runtime made the whole end-to-end check unverifiable there. The managed
download path remains the default and is still exercised when no --java-path
is supplied.
Mojang's downloads.client entry is a url/sha1/size triple with no Maven path.
Reusing Artifact.from_dict (which requires a path) silently produced None, so
every install failed at the client stage with "version ... declares no client
download". The same applied to logging.client.file.

Both are now built explicitly, defaulting the path to <id>.jar and the config
id respectively, with regression tests covering the real payload shape and the
absent case.
docs/TEST_REPORT.md lists the environment, every command that was run, the
observed output, the defects the runs exposed (including the client-jar parsing
bug that blocked every installation), and the capabilities that could not be
verified here and why. No result in it is projected.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-23T15:33:29.823420Z
We couldn’t complete this review. Comment @codex review to try again.
a086edb New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Apeiron Development added 4 commits October 23, 2026 22:50
The first CI run exposed three real problems, all fixed here:

* the lint job also covers packaging/, which had never been linted locally and
  failed on an unused noqa plus an outdated version guard;
* the translation completeness check parsed qsTr line-by-line, so a line with
  two calls (e.g. Stop/Launch) produced garbage and false "missing" strings. It
  now scans each file with a proper regular expression and reports the number
  of occurrences it verified;
* pip installs used trailing backslashes for continuation, which is a parse
  error under PowerShell, the default shell on windows runners.

Workflow YAML still validates (7 jobs in ci.yml, 1 in build.yml).
The first green-on-Linux run was hiding assumptions that only held here:

* the update-check tests hard-coded a "linux" asset entry, so on macOS and
  Windows the checker correctly found no asset and returned None. They now use
  current_platform_key(), which is what the code actually looks up.
* the launch test compared argv[0] against "/usr/bin/java"; on Windows the same
  Path renders with backslashes. It now compares against str(Path(...)).
* the GUI smoke asserted Chinese UI text while the product default is "follow
  system" and CI runners are English. The run now pins a Chinese locale, which
  is also the scenario the brief requires be tested (Chinese first start).
* Qt emits a "Detected locale C ... not UTF-8" warning on hosts without an
  installed Chinese locale. That is a property of the machine, so it is
  excluded from the no-warnings gate; any QML warning still fails the run.

The Windows GUI smoke crashes the offscreen scene graph (0xC0000005) and is
skipped there with an explicit reason rather than being reported as passing;
docs/TEST_REPORT.md records Windows GUI verification as not performed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43f7a16839

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +360 to +361
if allow_error_body and "HTTP 400" in exc.message:
return {"error": "invalid_request", "error_description": exc.detail or ""}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse the OAuth error body while polling

During a normal device-code login, Microsoft's token endpoint returns protocol states such as authorization_pending and slow_down as HTTP 400 JSON responses. HttpClient discards that response body, and this handler converts every such response into the fabricated invalid_request value, so wait_for_token() fails on its first poll instead of waiting for the user to authorize. Preserve and parse the JSON error body for this endpoint.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/mc/version.py
Comment on lines +176 to +179
def native_artifact(self, platform_key: str) -> Artifact | None:
classifier = self.natives.get(platform_key)
if not classifier:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map macOS to Mojang's native classifier key

On Darwin, callers pass current_platform_key() == "macos", but Mojang version metadata keys native classifiers as osx. Consequently this lookup returns None for every macOS native library, the installer skips the LWJGL native jars, and Minecraft cannot start on a platform the launcher claims to support. Translate the application platform name to Mojang's osx vocabulary for classifier and rule evaluation.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/build.yml Outdated
Comment on lines +66 to +70
if [ "$RUNNER_OS" = "Windows" ]; then
BIN="dist/__main__.dist/Apeiron.exe"
else
BIN="dist/__main__.dist/Apeiron"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the distribution directory produced from entry.py

The checked build workflow compiles packaging/entry.py, and packaging/build_nuitka.py itself searches entry.dist before __main__.dist; however, this smoke step hardcodes only dist/__main__.dist. When Nuitka uses the input module name and creates entry.dist, the build script's self-check succeeds but every matrix job then reports a missing artifact, and the archive step later hardcodes the same wrong directory.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/ui/backend.py
Comment on lines +446 to +449
if loader and loader != "vanilla" and loader_version:
provider = self.ctx.modpacks.loaders.get(loader)
if provider is not None:
result = provider.install(mc_version, loader_version)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required consent flow before installing Forge

When a user selects Forge or NeoForge in the new-instance dialog, this calls _InstallerJarLoader.install() without allow_execution=True; that method unconditionally rejects the call because no consent was supplied, and there is no subsequent confirmation path in the UI. The instance row and directory have already been created at this point, so the operation both fails every time and leaves an incomplete instance behind. Obtain explicit consent before creation or roll back and invoke the installer with that consent.

Useful? React with 👍 / 👎.

fileMode: FileDialog.SaveFile
defaultSuffix: "apeiron.zip"
nameFilters: [qsTr("Apeiron instance (*.zip)"), qsTr("All files (*)")]
onAccepted: App.exportInstance(App.selectedInstanceId, selectedFile)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Convert FileDialog URLs to local filesystem paths

Qt Quick FileDialog.selectedFile is a URL, so a normal selection is passed as a value such as file:///home/user/export.zip. The Python slots accept it as a string and feed it directly to Path, which treats that value as a relative filesystem path rather than /home/user/export.zip; export therefore writes to an unintended file: tree, while import and content installation report that the selected file is missing. Pass selectedFile.toLocalFile() or accept a QUrl and call toLocalFile() in the backend.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/ui/backend.py
Comment on lines +467 to +470
def deleteInstance(self, instance_id: str) -> None:
def work() -> dict[str, Any]:
instance = self.ctx.instances.get(instance_id)
self.ctx.instances.delete(instance_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse to delete an instance while its game is running

The Library keeps its Delete action available during a launch, and this backend path removes the selected instance without checking launcher.process or launcher.state.instance_id. On POSIX, deleting the active instance succeeds while Java still has it as its working directory, removing worlds and configuration underneath the running game and causing later saves to fail; on platforms with locked files it instead leaves a partially deleted instance. Disable the action or reject deletion when this instance is active.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/ui/backend.py
Comment on lines +1058 to +1062
def shutdown(self, timeout_ms: int = 5000) -> None:
"""Stop background work before the engine is destroyed."""
try:
self.ctx.launcher.stop()
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect the launcher-exit process setting

Settings.close_game_on_launcher_exit defaults to False, but every shutdown unconditionally calls launcher.stop() here, and the application shutdown hook calls it again. Consequently closing Apeiron always terminates an active Minecraft process even when the persisted setting says to leave it running. Gate the stop call on that setting so the default behavior does not kill the user's game.

Useful? React with 👍 / 👎.

Comment on lines +193 to +195
except Exception as exc:
log.error("loader install failed for %s %s: %s", loader, version, exc)
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate loader installation failures during modpack import

If the requested Fabric, Quilt, Forge, or NeoForge loader cannot be installed—for example because metadata is unavailable or Java is missing—this broad handler converts the failure to False. Both modpack import paths then continue installing files and return an ImportResult, so the UI reports a successful import even though the instance has no loaderVersionId and cannot launch. Let the loader error abort the import, ideally rolling back the newly created instance.

Useful? React with 👍 / 👎.

Comment on lines +130 to +135
for entry in index.files:
self._record_file(
instance,
instance_dir,
entry.path,
"mod",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve each Modrinth file's content category

Every downloaded Modrinth entry is recorded as kind mod, even when its path is under resourcepacks/, shaderpacks/, or datapacks/. Such files consequently appear in the Mods tab, and toggle/removal operations look for their basename under the instance's mods directory, delete the database record, and leave the actual content untouched. Derive the record kind and managed relative location from entry.path instead of hardcoding mod.

Useful? React with 👍 / 👎.

Comment on lines +183 to +188
DownloadSpec(
url=entry.downloads[0],
destination=destination,
sha1=entry.sha1,
size=entry.size,
category="modpack",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the SHA-512 declared by Modrinth packs

MrpackFile parses and retains sha512, but the generated DownloadSpec passes only sha1, and the downloader has no SHA-512 check. Because the parser also permits an entry with only a SHA-512 hash, such an untrusted pack causes the downloaded executable JAR to be committed with no checksum verification at all; even when both hashes exist, the stronger advertised digest is ignored. Require and verify the declared SHA-512 before installing the file.

Useful? React with 👍 / 👎.

Apeiron Development added 4 commits October 23, 2026 23:01
* Every update-check test now builds its manifest for current_platform_key();
  one of them still hard-coded a Linux asset entry, so macOS saw "no asset for
  this platform" and the checker correctly returned None.
* The UI font list is filtered against installed families. Requesting absent
  families made Qt build alias tables on macOS and emit a warning that the
  no-warnings gate (correctly) rejected; the platform default is kept when no
  preferred family exists.
* packaging/build_nuitka.py puts src/ on sys.path. CI runs it from a clean
  checkout without an installed distribution, where Nuitka failed with
  "failed to locate package 'apeiron'".
…I check

* Nuitka runs as a subprocess, so inserting src/ into the parent's sys.path was
  invisible to it and CI still failed with "failed to locate package
  'apeiron'". Child processes now receive PYTHONPATH including src/.
* macOS offscreen rendering emits "Populating font family aliases ... missing
  font family Sans Serif", which is a property of the host's font set, not of
  the interface. It joins the locale warning as a documented exception to the
  no-warnings gate; QML warnings still fail the run.
--include-package is resolved through the interpreter's import system, so a
subprocess started from a clean checkout could not see src/ even with
PYTHONPATH exported, and every platform failed with "failed to locate package
'apeiron'". The build script now performs an idempotent editable install first.
… name

Nuitka names the output directory after the compiled entry script, so the
workflow looked for __main__.dist while the build produced entry.dist and every
platform failed at the smoke step with a missing file.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

The extension is included as a data file so that it lands in apeiron/ exactly
as the frozen package expects. Nuitka's shared-library scanner also tried to
copy it, which collided with the data file and crashed on macOS with
NotADirectoryError; that scan is now excluded for _logcore.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a79148f3cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

uuid=uuid,
access_token=access_token,
user_type=user_type,
xuid=xuid,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the OAuth client ID into launch options

For Minecraft versions whose game arguments contain --clientId ${clientid}, this constructs LaunchOptions without the client ID even though the authenticated session carries it. The substitution consequently produces an empty value, and build_command() removes that value while retaining --clientId, leaving the following option to be parsed as its argument and preventing affected modern versions from launching. Thread the resolved OAuth client ID through Backend.launchInstance() and LaunchService.launch() into these options.

Useful? React with 👍 / 👎.

Comment on lines +167 to +168
with zf.open(info) as src, target.open("wb") as dst:
dst.write(src.read())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit override extraction before reading entries

When importing an untrusted modpack with a highly compressed override entry, src.read() materializes the entire decompressed file in memory and writes it without any aggregate or per-entry size limit. A crafted .mrpack can therefore exhaust memory or disk during a normal import; the CurseForge override extractor repeats the same pattern. Stream entries in bounded chunks while enforcing a decompressed-size budget, as the shared archive extractor already does.

Useful? React with 👍 / 👎.

Comment on lines +187 to +194
Component.onCompleted: {
for (var i = 0; i < App.accounts.length; ++i) {
if (App.accounts[i].id === App.selectedInstance.accountId) {
currentIndex = i
break
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resync the account selector when selection changes

This synchronization runs only when the component is first created, while the details panel remains instantiated as users select different instances. After switching from an instance using account A to one using account B, the combo box can still show A, and pressing Save passes A's ID to updateInstance(), silently rebinding the newly selected instance. Recompute currentIndex whenever App.selectedInstance.accountId or the account model changes.

Useful? React with 👍 / 👎.

Comment on lines +38 to +40
onAccepted: {
App.installContent(App.selectedInstanceId, selectedFile, page.kindFilter)
page.reload()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh content after background mutations finish

installContent() only schedules a worker task, so this immediate reload normally queries the database before the copy and record insertion complete. The backend completion handler emits only a notification, and the page listens only for selection/instance changes, so newly installed content remains absent until an unrelated refresh; removal and toggling have the same race. Emit a content-changed signal or reload from the task's completion callback instead.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/ui/backend.py
Comment on lines +972 to +974
def done(result: dict[str, Any]) -> None:
self.detect_java()
self.notify.emit("success", "java.installed", str(result.get("version", "")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist the managed Java runtime after installation

On a machine with no system Java—the primary scenario for these Install Java buttons—the successful runtime path is discarded and the callback merely rescans. detect_runtimes() searches system locations and configured extras, not ctx.paths.java, while the launcher is likewise wired to detect_runtimes() and an unchanged empty settings.java_path; the newly installed Temurin runtime therefore remains invisible and launching still reports that no Java exists. Save result["path"] as the configured runtime or include managed runtime directories in discovery.

Useful? React with 👍 / 👎.

Comment on lines +173 to +174
files = self.curseforge.install_files(manifest, instance_dir, self.downloader)
overrides = curseforge.extract_overrides(archive, manifest, instance_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record downloaded CurseForge files in the content repository

After a CurseForge import succeeds, this downloads all required mods but never creates corresponding ModRecord rows, unlike the Modrinth path. The Content page consequently reports no installed mods, toggle/removal cannot manage them, and exporting the instance as an mrpack omits every CurseForge-installed file. Preserve the resolved file metadata and add records after successful downloads.

Useful? React with 👍 / 👎.

Comment on lines +199 to +200
code = self.process.stop()
self.state = LaunchState(instance_id=self.state.instance_id, phase="exited", exit_code=code)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve launch start time while stopping the game

When the user stops a running game, GameProcess.stop() and the pump thread's exit callback race after the child exits. If this assignment wins before _exit() computes its duration, it replaces self.state with a new state whose started_at is zero, so _exit() records a zero-second session and undercounts play time. Capture the launch timestamp in the callback closure or wait for the exit callback instead of overwriting the state here.

Useful? React with 👍 / 👎.

Comment on lines +156 to +158
command = build_command(installed, options)
log_path = create_launch_log(instance_dir)
record = self.launches.start(instance.id, str(log_path))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Finalize the launch record when process startup fails

If GameProcess.start() raises—for example because the selected Java executable loses execute permission between probing and launch—the database row has already been inserted with status running, but the exit callback is never installed on a live process to finish it. The failed attempt then appears as running forever in recent launches. Create the record after successful process startup or mark it failed in the startup exception path.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/mc/launch.py
Comment on lines +83 to +85
try:
return shlex.split(value, posix=True)
except ValueError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Windows backslashes in custom launch arguments

On Windows, an unquoted custom argument such as -Dconfig=C:\mods\config.json is parsed with POSIX shell rules, which treat each backslash as an escape and produce -Dconfig=C:modsconfig.json. Since the settings UI exposes custom JVM arguments, ordinary Windows paths are silently corrupted before Java is started. Use platform-appropriate argument parsing or a parser that preserves Windows path separators.

Useful? React with 👍 / 👎.

Comment thread src/apeiron/ui/backend.py
Comment on lines +303 to +306
def _on_done(self, task_id: str, result: Any) -> None:
callback = self._pending.pop(task_id, None)
self._busy = ""
self.busyChanged.emit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear completed download status when background work ends

Every downloader callback sets status.kind to download, including the final callback with all tasks complete, but successful and failed task handling only clears _busy and never transitions that status. After an install or import finishes, the app bar therefore continues showing a running spinner and the Downloads page keeps presenting the transfer as current until another operation happens to replace the status. Reset it to idle or a terminal state when the owning task completes or fails.

Useful? React with 👍 / 👎.

…failures

CI is green on all 14 jobs across three operating systems and two Python
versions, and Linux packaging succeeds inside CI. macOS and Windows packaging
still fail at the Nuitka stage; the cause is not diagnosed and no artifact is
claimed for them. The ten defects the CI loop exposed are listed individually.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@hekuo5310
hekuo5310 merged commit 8b9f583 into main Oct 28, 2026
15 of 17 checks passed
hekuo5310 added a commit that referenced this pull request Oct 29, 2026
修复 PR #1 审查问题、跨平台构建并接入优先镜像下载源
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant