Skip to content

Bugfix/custom worlds again#48

Merged
lallaria merged 8 commits into
mainfrom
bugfix/custom_worlds_again
Jun 12, 2026
Merged

Bugfix/custom worlds again#48
lallaria merged 8 commits into
mainfrom
bugfix/custom_worlds_again

Conversation

@lallaria

Copy link
Copy Markdown
Contributor

Fixing custom worlds reading into the client.

lallaria and others added 8 commits June 6, 2026 10:31
… logging

Custom worlds are selectable only if their slug is in both
get_available_worlds() and GameIndex.search(); the on-launch scan must add
each apworld to the search index by game name (never importing the module,
they are zipfiles). This kept regressing because the test stub's search()
returned {} and add_game indexed the slug rather than the name, so the suite
gave false confidence while the real launcher could not find the world.

- test/_stubs/mwgg_igdb.py: stub now mirrors the real GameIndex -- add_game
  indexes the display name and search() does term/substring matching. No
  other test calls search(), so the blast radius is nil.
- test/general/test_custom_world_scan.py: add an end-to-end regression test
  (scanned, searchable-by-name with name words disjoint from the slug,
  resolvable both ways, persists across a rescan, surfaced by
  get_available_worlds(), never imports worlds.<slug>) plus a direct
  add_game->search-index test, behind an autouse fixture that
  snapshots/restores the GameIndex singleton.
- MultiWorld.py / Utils.py: log the swallowed scan failures with exc_info so
  a future break is diagnosable instead of invisible (still non-fatal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
custom_worlds was redirected to write_path() (AppData/Local on Windows,
~/.local/share on Linux, Application Support on macOS) for frozen builds in
#15, but upstream -- and every other consumer, Utils.set_game_names and the
launch path -- look next to the executable via local_path("custom_worlds").
So a frozen build scanned a different folder than where users drop apworlds
and where the launch path reads them, and custom worlds silently stopped
being selectable.

- ModuleUpdate.py: custom_worlds_dir is now a single source of truth
  (_resolve_custom_worlds_dir -> local_path), identical for dev and frozen.
  Drop the now-unused write_path import.
- Utils.py: set_game_names and discover_and_launch_module reference
  ModuleUpdate.custom_worlds_dir instead of recomputing local_path, so the
  scan and launch paths cannot drift apart again.
- test/general/test_custom_world_scan.py: tripwire test that fails if a
  frozen build is ever pointed away from the executable's folder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Custom-world clients such as kh3 import canonical kivy/kivymd names (Clock, Window, MDButton, MDButtonText, MDGridLayout, MDIconButton, MDTextField) from kvui. The mwgg_gui GUI shim exposed some only under MWGG aliases (ToggleButton, MainLayout) or not at all, raising ImportError at client launch. Restore the canonical re-exports in the GUI branch; existing aliases preserved; TUI branch unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
World clients subclass HoverBehavior with kivymd widgets, e.g. class KH3TooltipIconButton(HoverBehavior, MDIconButton). Re-exporting kivymd.uix.behaviors.HoverBehavior gave them a base whose MRO is incompatible with MDIconButton (TypeError: Cannot create a consistent method resolution order). Restore the original MWGG object-based HoverBehavior mixin (hovered/border_point, on_enter/on_leave), which linearizes cleanly with any widget and matches the API those clients expect. The only in-tree consumer, worlds/tracker/gui.py, stays compatible; its border_point re-declaration is now redundant but harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… clients

Per-world GUI clients (kh2/albw/kh3) begin run_gui() with an unconditional
`from kvui import (Clock, GameManager, HoverBehavior, MDBoxLayout, ...)`.
Under MWGG_FRONTEND=tui the TUI branch defined none of those names, so the
client died with `ImportError: cannot import name 'Clock' from 'kvui'` at
launch -- and the branch must not import Kivy (importing kivy.core.window
opens a rogue window over the Textual TUI).

Provide inert, non-Kivy stand-ins instead: an _Inert base + _InertMeta
metaclass (so stand-in classes tolerate class-level access like
Clock.schedule_interval), dp/sp that return their argument, Clock/Window as
the inert singleton, a widened GameManager.__init__ with an inert __getattr__,
and a PEP 562 module __getattr__ catch-all that mints a cached _Inert subclass
for any non-dunder name. Safe because the Kivy per-world UI is never built
under the TUI -- LegacyKvuiClientBuilder.build() returns early when
MWGG_FRONTEND=tui -- so the names only need to survive import, subclassing,
instantiation and attribute access; run_gui() then reaches the existing
takeover via GameManager.async_run(). GUI (else) branch unchanged.

Add test/general/test_kvui_tui.py (clean subprocess; asserts the world-client
import succeeds, stand-ins subclass/instantiate, the takeover handshake still
fires, and `kivy not in sys.modules`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ld clients

World clients written against websockets 13.x (shipped MWGG) check server liveness via ctx.server.socket.closed (kh3 alone has 4 such sites); the websockets 14+ asyncio Connection removed closed/open, so those checks raise AttributeError under the bundled websockets 16. Patch them back onto Connection as State-backed properties with legacy semantics (both False while opening/closing) when CommonClient is imported. Migrate worlds/_bizhawk/context.py's two remaining legacy .closed checks to the canonical 'state is not State.CLOSED' pattern so in-tree code does not depend on the shim. Add tests pinning the compat surface and its semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ll site

The websockets closed/open compat properties now log a one-time warning per offending call site (world file + line) to the Client logger, so world authors can see in the client log exactly where their world still uses the legacy websockets API and migrate it. Warnings are deduplicated per site rather than emitted on every access because these checks sit inside per-package and game-watcher loops.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eports

CI runs pytest with xdist; execnet cannot serialize the websockets State enum used as a subTest param, failing the two semantics tests with DumpError on every worker (the assertions themselves all pass). Use the member name string instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lallaria lallaria merged commit 0891f39 into main Jun 12, 2026
23 checks passed
@lallaria lallaria deleted the bugfix/custom_worlds_again branch June 15, 2026 14:34
lallaria added a commit that referenced this pull request Jun 15, 2026
* test(custom_worlds): lock down search-index registration; harden scan logging

Custom worlds are selectable only if their slug is in both
get_available_worlds() and GameIndex.search(); the on-launch scan must add
each apworld to the search index by game name (never importing the module,
they are zipfiles). This kept regressing because the test stub's search()
returned {} and add_game indexed the slug rather than the name, so the suite
gave false confidence while the real launcher could not find the world.

- test/_stubs/mwgg_igdb.py: stub now mirrors the real GameIndex -- add_game
  indexes the display name and search() does term/substring matching. No
  other test calls search(), so the blast radius is nil.
- test/general/test_custom_world_scan.py: add an end-to-end regression test
  (scanned, searchable-by-name with name words disjoint from the slug,
  resolvable both ways, persists across a rescan, surfaced by
  get_available_worlds(), never imports worlds.<slug>) plus a direct
  add_game->search-index test, behind an autouse fixture that
  snapshots/restores the GameIndex singleton.
- MultiWorld.py / Utils.py: log the swallowed scan failures with exc_info so
  a future break is diagnosable instead of invisible (still non-fatal).

* fix(custom_worlds): scan the executable's folder, not write_path/AppData

custom_worlds was redirected to write_path() (AppData/Local on Windows,
~/.local/share on Linux, Application Support on macOS) for frozen builds in
launch path -- look next to the executable via local_path("custom_worlds").
So a frozen build scanned a different folder than where users drop apworlds
and where the launch path reads them, and custom worlds silently stopped
being selectable.

- ModuleUpdate.py: custom_worlds_dir is now a single source of truth
  (_resolve_custom_worlds_dir -> local_path), identical for dev and frozen.
  Drop the now-unused write_path import.
- Utils.py: set_game_names and discover_and_launch_module reference
  ModuleUpdate.custom_worlds_dir instead of recomputing local_path, so the
  scan and launch paths cannot drift apart again.
- test/general/test_custom_world_scan.py: tripwire test that fails if a
  frozen build is ever pointed away from the executable's folder.

* fix(kvui): re-export Clock/Window/MD* names for world clients

Custom-world clients such as kh3 import canonical kivy/kivymd names (Clock, Window, MDButton, MDButtonText, MDGridLayout, MDIconButton, MDTextField) from kvui. The mwgg_gui GUI shim exposed some only under MWGG aliases (ToggleButton, MainLayout) or not at all, raising ImportError at client launch. Restore the canonical re-exports in the GUI branch; existing aliases preserved; TUI branch unchanged.

* fix(kvui): export object-based HoverBehavior mixin for world clients

World clients subclass HoverBehavior with kivymd widgets, e.g. class KH3TooltipIconButton(HoverBehavior, MDIconButton). Re-exporting kivymd.uix.behaviors.HoverBehavior gave them a base whose MRO is incompatible with MDIconButton (TypeError: Cannot create a consistent method resolution order). Restore the original MWGG object-based HoverBehavior mixin (hovered/border_point, on_enter/on_leave), which linearizes cleanly with any widget and matches the API those clients expect. The only in-tree consumer, worlds/tracker/gui.py, stays compatible; its border_point re-declaration is now redundant but harmless.

* fix(kvui): serve inert non-kivy stand-ins on the TUI branch for world clients

Per-world GUI clients (kh2/albw/kh3) begin run_gui() with an unconditional
`from kvui import (Clock, GameManager, HoverBehavior, MDBoxLayout, ...)`.
Under MWGG_FRONTEND=tui the TUI branch defined none of those names, so the
client died with `ImportError: cannot import name 'Clock' from 'kvui'` at
launch -- and the branch must not import Kivy (importing kivy.core.window
opens a rogue window over the Textual TUI).

Provide inert, non-Kivy stand-ins instead: an _Inert base + _InertMeta
metaclass (so stand-in classes tolerate class-level access like
Clock.schedule_interval), dp/sp that return their argument, Clock/Window as
the inert singleton, a widened GameManager.__init__ with an inert __getattr__,
and a PEP 562 module __getattr__ catch-all that mints a cached _Inert subclass
for any non-dunder name. Safe because the Kivy per-world UI is never built
under the TUI -- LegacyKvuiClientBuilder.build() returns early when
MWGG_FRONTEND=tui -- so the names only need to survive import, subclassing,
instantiation and attribute access; run_gui() then reaches the existing
takeover via GameManager.async_run(). GUI (else) branch unchanged.

Add test/general/test_kvui_tui.py (clean subprocess; asserts the world-client
import succeeds, stand-ins subclass/instantiate, the takeover handshake still
fires, and `kivy not in sys.modules`).

* fix(client): restore legacy websockets closed/open properties for world clients

World clients written against websockets 13.x (shipped MWGG) check server liveness via ctx.server.socket.closed (kh3 alone has 4 such sites); the websockets 14+ asyncio Connection removed closed/open, so those checks raise AttributeError under the bundled websockets 16. Patch them back onto Connection as State-backed properties with legacy semantics (both False while opening/closing) when CommonClient is imported. Migrate worlds/_bizhawk/context.py's two remaining legacy .closed checks to the canonical 'state is not State.CLOSED' pattern so in-tree code does not depend on the shim. Add tests pinning the compat surface and its semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(client): surface a deprecation warning per legacy socket attr call site

The websockets closed/open compat properties now log a one-time warning per offending call site (world file + line) to the Client logger, so world authors can see in the client log exactly where their world still uses the legacy websockets API and migrate it. Warnings are deduplicated per site rather than emitted on every access because these checks sit inside per-package and game-watcher loops.

* fix(test): pass State.name to subTest so pytest-xdist can serialize reports

CI runs pytest with xdist; execnet cannot serialize the websockets State enum used as a subTest param, failing the two semantics tests with DumpError on every worker (the assertions themselves all pass). Use the member name string instead.
lallaria added a commit that referenced this pull request Jun 15, 2026
* test(custom_worlds): lock down search-index registration; harden scan logging

Custom worlds are selectable only if their slug is in both
get_available_worlds() and GameIndex.search(); the on-launch scan must add
each apworld to the search index by game name (never importing the module,
they are zipfiles). This kept regressing because the test stub's search()
returned {} and add_game indexed the slug rather than the name, so the suite
gave false confidence while the real launcher could not find the world.

- test/_stubs/mwgg_igdb.py: stub now mirrors the real GameIndex -- add_game
  indexes the display name and search() does term/substring matching. No
  other test calls search(), so the blast radius is nil.
- test/general/test_custom_world_scan.py: add an end-to-end regression test
  (scanned, searchable-by-name with name words disjoint from the slug,
  resolvable both ways, persists across a rescan, surfaced by
  get_available_worlds(), never imports worlds.<slug>) plus a direct
  add_game->search-index test, behind an autouse fixture that
  snapshots/restores the GameIndex singleton.
- MultiWorld.py / Utils.py: log the swallowed scan failures with exc_info so
  a future break is diagnosable instead of invisible (still non-fatal).

* fix(custom_worlds): scan the executable's folder, not write_path/AppData

custom_worlds was redirected to write_path() (AppData/Local on Windows,
~/.local/share on Linux, Application Support on macOS) for frozen builds in
launch path -- look next to the executable via local_path("custom_worlds").
So a frozen build scanned a different folder than where users drop apworlds
and where the launch path reads them, and custom worlds silently stopped
being selectable.

- ModuleUpdate.py: custom_worlds_dir is now a single source of truth
  (_resolve_custom_worlds_dir -> local_path), identical for dev and frozen.
  Drop the now-unused write_path import.
- Utils.py: set_game_names and discover_and_launch_module reference
  ModuleUpdate.custom_worlds_dir instead of recomputing local_path, so the
  scan and launch paths cannot drift apart again.
- test/general/test_custom_world_scan.py: tripwire test that fails if a
  frozen build is ever pointed away from the executable's folder.

* fix(kvui): re-export Clock/Window/MD* names for world clients

Custom-world clients such as kh3 import canonical kivy/kivymd names (Clock, Window, MDButton, MDButtonText, MDGridLayout, MDIconButton, MDTextField) from kvui. The mwgg_gui GUI shim exposed some only under MWGG aliases (ToggleButton, MainLayout) or not at all, raising ImportError at client launch. Restore the canonical re-exports in the GUI branch; existing aliases preserved; TUI branch unchanged.

* fix(kvui): export object-based HoverBehavior mixin for world clients

World clients subclass HoverBehavior with kivymd widgets, e.g. class KH3TooltipIconButton(HoverBehavior, MDIconButton). Re-exporting kivymd.uix.behaviors.HoverBehavior gave them a base whose MRO is incompatible with MDIconButton (TypeError: Cannot create a consistent method resolution order). Restore the original MWGG object-based HoverBehavior mixin (hovered/border_point, on_enter/on_leave), which linearizes cleanly with any widget and matches the API those clients expect. The only in-tree consumer, worlds/tracker/gui.py, stays compatible; its border_point re-declaration is now redundant but harmless.

* fix(kvui): serve inert non-kivy stand-ins on the TUI branch for world clients

Per-world GUI clients (kh2/albw/kh3) begin run_gui() with an unconditional
`from kvui import (Clock, GameManager, HoverBehavior, MDBoxLayout, ...)`.
Under MWGG_FRONTEND=tui the TUI branch defined none of those names, so the
client died with `ImportError: cannot import name 'Clock' from 'kvui'` at
launch -- and the branch must not import Kivy (importing kivy.core.window
opens a rogue window over the Textual TUI).

Provide inert, non-Kivy stand-ins instead: an _Inert base + _InertMeta
metaclass (so stand-in classes tolerate class-level access like
Clock.schedule_interval), dp/sp that return their argument, Clock/Window as
the inert singleton, a widened GameManager.__init__ with an inert __getattr__,
and a PEP 562 module __getattr__ catch-all that mints a cached _Inert subclass
for any non-dunder name. Safe because the Kivy per-world UI is never built
under the TUI -- LegacyKvuiClientBuilder.build() returns early when
MWGG_FRONTEND=tui -- so the names only need to survive import, subclassing,
instantiation and attribute access; run_gui() then reaches the existing
takeover via GameManager.async_run(). GUI (else) branch unchanged.

Add test/general/test_kvui_tui.py (clean subprocess; asserts the world-client
import succeeds, stand-ins subclass/instantiate, the takeover handshake still
fires, and `kivy not in sys.modules`).

* fix(client): restore legacy websockets closed/open properties for world clients

World clients written against websockets 13.x (shipped MWGG) check server liveness via ctx.server.socket.closed (kh3 alone has 4 such sites); the websockets 14+ asyncio Connection removed closed/open, so those checks raise AttributeError under the bundled websockets 16. Patch them back onto Connection as State-backed properties with legacy semantics (both False while opening/closing) when CommonClient is imported. Migrate worlds/_bizhawk/context.py's two remaining legacy .closed checks to the canonical 'state is not State.CLOSED' pattern so in-tree code does not depend on the shim. Add tests pinning the compat surface and its semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(client): surface a deprecation warning per legacy socket attr call site

The websockets closed/open compat properties now log a one-time warning per offending call site (world file + line) to the Client logger, so world authors can see in the client log exactly where their world still uses the legacy websockets API and migrate it. Warnings are deduplicated per site rather than emitted on every access because these checks sit inside per-package and game-watcher loops.

* fix(test): pass State.name to subTest so pytest-xdist can serialize reports

CI runs pytest with xdist; execnet cannot serialize the websockets State enum used as a subTest param, failing the two semantics tests with DumpError on every worker (the assertions themselves all pass). Use the member name string instead.
lallaria added a commit that referenced this pull request Jun 15, 2026
* test(custom_worlds): lock down search-index registration; harden scan logging

Custom worlds are selectable only if their slug is in both
get_available_worlds() and GameIndex.search(); the on-launch scan must add
each apworld to the search index by game name (never importing the module,
they are zipfiles). This kept regressing because the test stub's search()
returned {} and add_game indexed the slug rather than the name, so the suite
gave false confidence while the real launcher could not find the world.

- test/_stubs/mwgg_igdb.py: stub now mirrors the real GameIndex -- add_game
  indexes the display name and search() does term/substring matching. No
  other test calls search(), so the blast radius is nil.
- test/general/test_custom_world_scan.py: add an end-to-end regression test
  (scanned, searchable-by-name with name words disjoint from the slug,
  resolvable both ways, persists across a rescan, surfaced by
  get_available_worlds(), never imports worlds.<slug>) plus a direct
  add_game->search-index test, behind an autouse fixture that
  snapshots/restores the GameIndex singleton.
- MultiWorld.py / Utils.py: log the swallowed scan failures with exc_info so
  a future break is diagnosable instead of invisible (still non-fatal).

* fix(custom_worlds): scan the executable's folder, not write_path/AppData

custom_worlds was redirected to write_path() (AppData/Local on Windows,
~/.local/share on Linux, Application Support on macOS) for frozen builds in
launch path -- look next to the executable via local_path("custom_worlds").
So a frozen build scanned a different folder than where users drop apworlds
and where the launch path reads them, and custom worlds silently stopped
being selectable.

- ModuleUpdate.py: custom_worlds_dir is now a single source of truth
  (_resolve_custom_worlds_dir -> local_path), identical for dev and frozen.
  Drop the now-unused write_path import.
- Utils.py: set_game_names and discover_and_launch_module reference
  ModuleUpdate.custom_worlds_dir instead of recomputing local_path, so the
  scan and launch paths cannot drift apart again.
- test/general/test_custom_world_scan.py: tripwire test that fails if a
  frozen build is ever pointed away from the executable's folder.

* fix(kvui): re-export Clock/Window/MD* names for world clients

Custom-world clients such as kh3 import canonical kivy/kivymd names (Clock, Window, MDButton, MDButtonText, MDGridLayout, MDIconButton, MDTextField) from kvui. The mwgg_gui GUI shim exposed some only under MWGG aliases (ToggleButton, MainLayout) or not at all, raising ImportError at client launch. Restore the canonical re-exports in the GUI branch; existing aliases preserved; TUI branch unchanged.

* fix(kvui): export object-based HoverBehavior mixin for world clients

World clients subclass HoverBehavior with kivymd widgets, e.g. class KH3TooltipIconButton(HoverBehavior, MDIconButton). Re-exporting kivymd.uix.behaviors.HoverBehavior gave them a base whose MRO is incompatible with MDIconButton (TypeError: Cannot create a consistent method resolution order). Restore the original MWGG object-based HoverBehavior mixin (hovered/border_point, on_enter/on_leave), which linearizes cleanly with any widget and matches the API those clients expect. The only in-tree consumer, worlds/tracker/gui.py, stays compatible; its border_point re-declaration is now redundant but harmless.

* fix(kvui): serve inert non-kivy stand-ins on the TUI branch for world clients

Per-world GUI clients (kh2/albw/kh3) begin run_gui() with an unconditional
`from kvui import (Clock, GameManager, HoverBehavior, MDBoxLayout, ...)`.
Under MWGG_FRONTEND=tui the TUI branch defined none of those names, so the
client died with `ImportError: cannot import name 'Clock' from 'kvui'` at
launch -- and the branch must not import Kivy (importing kivy.core.window
opens a rogue window over the Textual TUI).

Provide inert, non-Kivy stand-ins instead: an _Inert base + _InertMeta
metaclass (so stand-in classes tolerate class-level access like
Clock.schedule_interval), dp/sp that return their argument, Clock/Window as
the inert singleton, a widened GameManager.__init__ with an inert __getattr__,
and a PEP 562 module __getattr__ catch-all that mints a cached _Inert subclass
for any non-dunder name. Safe because the Kivy per-world UI is never built
under the TUI -- LegacyKvuiClientBuilder.build() returns early when
MWGG_FRONTEND=tui -- so the names only need to survive import, subclassing,
instantiation and attribute access; run_gui() then reaches the existing
takeover via GameManager.async_run(). GUI (else) branch unchanged.

Add test/general/test_kvui_tui.py (clean subprocess; asserts the world-client
import succeeds, stand-ins subclass/instantiate, the takeover handshake still
fires, and `kivy not in sys.modules`).

* fix(client): restore legacy websockets closed/open properties for world clients

World clients written against websockets 13.x (shipped MWGG) check server liveness via ctx.server.socket.closed (kh3 alone has 4 such sites); the websockets 14+ asyncio Connection removed closed/open, so those checks raise AttributeError under the bundled websockets 16. Patch them back onto Connection as State-backed properties with legacy semantics (both False while opening/closing) when CommonClient is imported. Migrate worlds/_bizhawk/context.py's two remaining legacy .closed checks to the canonical 'state is not State.CLOSED' pattern so in-tree code does not depend on the shim. Add tests pinning the compat surface and its semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(client): surface a deprecation warning per legacy socket attr call site

The websockets closed/open compat properties now log a one-time warning per offending call site (world file + line) to the Client logger, so world authors can see in the client log exactly where their world still uses the legacy websockets API and migrate it. Warnings are deduplicated per site rather than emitted on every access because these checks sit inside per-package and game-watcher loops.

* fix(test): pass State.name to subTest so pytest-xdist can serialize reports

CI runs pytest with xdist; execnet cannot serialize the websockets State enum used as a subTest param, failing the two semantics tests with DumpError on every worker (the assertions themselves all pass). Use the member name string instead.
lallaria added a commit that referenced this pull request Jun 15, 2026
* test(custom_worlds): lock down search-index registration; harden scan logging

Custom worlds are selectable only if their slug is in both
get_available_worlds() and GameIndex.search(); the on-launch scan must add
each apworld to the search index by game name (never importing the module,
they are zipfiles). This kept regressing because the test stub's search()
returned {} and add_game indexed the slug rather than the name, so the suite
gave false confidence while the real launcher could not find the world.

- test/_stubs/mwgg_igdb.py: stub now mirrors the real GameIndex -- add_game
  indexes the display name and search() does term/substring matching. No
  other test calls search(), so the blast radius is nil.
- test/general/test_custom_world_scan.py: add an end-to-end regression test
  (scanned, searchable-by-name with name words disjoint from the slug,
  resolvable both ways, persists across a rescan, surfaced by
  get_available_worlds(), never imports worlds.<slug>) plus a direct
  add_game->search-index test, behind an autouse fixture that
  snapshots/restores the GameIndex singleton.
- MultiWorld.py / Utils.py: log the swallowed scan failures with exc_info so
  a future break is diagnosable instead of invisible (still non-fatal).

* fix(custom_worlds): scan the executable's folder, not write_path/AppData

custom_worlds was redirected to write_path() (AppData/Local on Windows,
~/.local/share on Linux, Application Support on macOS) for frozen builds in
launch path -- look next to the executable via local_path("custom_worlds").
So a frozen build scanned a different folder than where users drop apworlds
and where the launch path reads them, and custom worlds silently stopped
being selectable.

- ModuleUpdate.py: custom_worlds_dir is now a single source of truth
  (_resolve_custom_worlds_dir -> local_path), identical for dev and frozen.
  Drop the now-unused write_path import.
- Utils.py: set_game_names and discover_and_launch_module reference
  ModuleUpdate.custom_worlds_dir instead of recomputing local_path, so the
  scan and launch paths cannot drift apart again.
- test/general/test_custom_world_scan.py: tripwire test that fails if a
  frozen build is ever pointed away from the executable's folder.

* fix(kvui): re-export Clock/Window/MD* names for world clients

Custom-world clients such as kh3 import canonical kivy/kivymd names (Clock, Window, MDButton, MDButtonText, MDGridLayout, MDIconButton, MDTextField) from kvui. The mwgg_gui GUI shim exposed some only under MWGG aliases (ToggleButton, MainLayout) or not at all, raising ImportError at client launch. Restore the canonical re-exports in the GUI branch; existing aliases preserved; TUI branch unchanged.

* fix(kvui): export object-based HoverBehavior mixin for world clients

World clients subclass HoverBehavior with kivymd widgets, e.g. class KH3TooltipIconButton(HoverBehavior, MDIconButton). Re-exporting kivymd.uix.behaviors.HoverBehavior gave them a base whose MRO is incompatible with MDIconButton (TypeError: Cannot create a consistent method resolution order). Restore the original MWGG object-based HoverBehavior mixin (hovered/border_point, on_enter/on_leave), which linearizes cleanly with any widget and matches the API those clients expect. The only in-tree consumer, worlds/tracker/gui.py, stays compatible; its border_point re-declaration is now redundant but harmless.

* fix(kvui): serve inert non-kivy stand-ins on the TUI branch for world clients

Per-world GUI clients (kh2/albw/kh3) begin run_gui() with an unconditional
`from kvui import (Clock, GameManager, HoverBehavior, MDBoxLayout, ...)`.
Under MWGG_FRONTEND=tui the TUI branch defined none of those names, so the
client died with `ImportError: cannot import name 'Clock' from 'kvui'` at
launch -- and the branch must not import Kivy (importing kivy.core.window
opens a rogue window over the Textual TUI).

Provide inert, non-Kivy stand-ins instead: an _Inert base + _InertMeta
metaclass (so stand-in classes tolerate class-level access like
Clock.schedule_interval), dp/sp that return their argument, Clock/Window as
the inert singleton, a widened GameManager.__init__ with an inert __getattr__,
and a PEP 562 module __getattr__ catch-all that mints a cached _Inert subclass
for any non-dunder name. Safe because the Kivy per-world UI is never built
under the TUI -- LegacyKvuiClientBuilder.build() returns early when
MWGG_FRONTEND=tui -- so the names only need to survive import, subclassing,
instantiation and attribute access; run_gui() then reaches the existing
takeover via GameManager.async_run(). GUI (else) branch unchanged.

Add test/general/test_kvui_tui.py (clean subprocess; asserts the world-client
import succeeds, stand-ins subclass/instantiate, the takeover handshake still
fires, and `kivy not in sys.modules`).

* fix(client): restore legacy websockets closed/open properties for world clients

World clients written against websockets 13.x (shipped MWGG) check server liveness via ctx.server.socket.closed (kh3 alone has 4 such sites); the websockets 14+ asyncio Connection removed closed/open, so those checks raise AttributeError under the bundled websockets 16. Patch them back onto Connection as State-backed properties with legacy semantics (both False while opening/closing) when CommonClient is imported. Migrate worlds/_bizhawk/context.py's two remaining legacy .closed checks to the canonical 'state is not State.CLOSED' pattern so in-tree code does not depend on the shim. Add tests pinning the compat surface and its semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(client): surface a deprecation warning per legacy socket attr call site

The websockets closed/open compat properties now log a one-time warning per offending call site (world file + line) to the Client logger, so world authors can see in the client log exactly where their world still uses the legacy websockets API and migrate it. Warnings are deduplicated per site rather than emitted on every access because these checks sit inside per-package and game-watcher loops.

* fix(test): pass State.name to subTest so pytest-xdist can serialize reports

CI runs pytest with xdist; execnet cannot serialize the websockets State enum used as a subTest param, failing the two semantics tests with DumpError on every worker (the assertions themselves all pass). Use the member name string instead.
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