Bugfix/custom worlds again#48
Merged
Merged
Conversation
… 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
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.
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.
Fixing custom worlds reading into the client.