Skip to content

Commit f3ca652

Browse files
committed
remove __none__
1 parent c3c6a4f commit f3ca652

4 files changed

Lines changed: 256 additions & 32 deletions

File tree

‎game/Submods/MAICA_ChatSubmod/trigger.rpy‎

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ init 999 python in maica:
3333
except Exception:
3434
pass
3535

36+
def _reserved_mtrigger_item_error(display_name):
37+
if isinstance(display_name, basestring) and display_name == "__none__":
38+
return "reserved legacy item name"
39+
return None
40+
3641
def _add_mtrigger_item(target, trigger_name, source, display_name, mapped_value,
3742
source_key=None, index=None):
38-
reason = add_valid_mtrigger_item(target, display_name, mapped_value)
43+
reason = _reserved_mtrigger_item_error(display_name)
44+
if reason is None:
45+
reason = add_valid_mtrigger_item(target, display_name, mapped_value)
3946
if reason is not None:
4047
log_invalid_mtrigger(
4148
trigger_name,
@@ -121,13 +128,6 @@ init 999 python in maica:
121128
"玩家挑选",
122129
"mas_pick_a_clothes",
123130
)
124-
_add_mtrigger_item(
125-
self.clothes_data,
126-
"clothes",
127-
"built-in fallback",
128-
"__none__",
129-
"mas_pick_a_clothes",
130-
)
131131
source = "store.mas_selspr.CLOTH_SEL_MAP"
132132
try:
133133
clothes_map = store.mas_selspr.CLOTH_SEL_MAP
@@ -222,6 +222,8 @@ init 999 python in maica:
222222
self.callback(clothes)
223223

224224
def clothes_callback(self, clothes):
225+
if clothes == "__none__":
226+
clothes = "玩家挑选"
225227
if not _valid_mtrigger_selection(self.clothes_data, "clothes", clothes):
226228
return
227229
return store.renpy.call("mtrigger_change_clothes", self.clothes_data[clothes])
@@ -234,7 +236,6 @@ init 999 python in maica:
234236
def get_unlocked_games():
235237
games = {}
236238
_add_mtrigger_item(games, "minigame", "built-in fallback", "玩家自行选择", "mas_pick_a_game")
237-
_add_mtrigger_item(games, "minigame", "built-in fallback", "__none__", "mas_pick_a_game")
238239
_add_mtrigger_item(games, "minigame", "built-in fallback", "Pong", "game_pong")
239240
try:
240241
hangman_unlocked = (
@@ -346,6 +347,8 @@ init 999 python in maica:
346347

347348
if item is None:
348349
return
350+
if item == "__none__":
351+
item = "玩家自行选择"
349352
if not _valid_mtrigger_selection(unlocked_games_dict, "minigame", item):
350353
return
351354
game_label = unlocked_games_dict[item]
@@ -478,13 +481,6 @@ init 999 python in maica:
478481
def get_weather_dict(self):
479482
source = "store.mas_weather.WEATHER_MAP"
480483
weathers = {}
481-
_add_mtrigger_item(
482-
weathers,
483-
"weather",
484-
"built-in fallback",
485-
"__none__",
486-
None,
487-
)
488484
try:
489485
import store.mas_weather as mas_weather
490486
except Exception as error:
@@ -699,8 +695,8 @@ init 999 python in maica:
699695
return current if current in self.musics else None
700696

701697
def song_list(self):
702-
m = ["__none__"]
703-
seen = {"__none__": True}
698+
m = []
699+
seen = {}
704700
reserved = {
705701
"__none__": True,
706702
self.PLAYER_CHOICE: True,
@@ -889,13 +885,6 @@ init 999 python in maica:
889885
"玩家挑选",
890886
"mas_pick_a_clothes",
891887
)
892-
_add_mtrigger_item(
893-
self.clothes_data,
894-
"hair",
895-
"built-in fallback",
896-
"__none__",
897-
"mas_pick_a_clothes",
898-
)
899888
source = "store.mas_selspr.HAIR_SEL_MAP"
900889
try:
901890
hair_map = store.mas_selspr.HAIR_SEL_MAP
@@ -992,6 +981,8 @@ init 999 python in maica:
992981
self.callback(clothes)
993982

994983
def clothes_callback(self, clothes):
984+
if clothes == "__none__":
985+
clothes = "玩家挑选"
995986
if not _valid_mtrigger_selection(self.clothes_data, "hair", clothes):
996987
return
997988
return store.renpy.call("mtrigger_change_hair", self.clothes_data[clothes])
@@ -1021,7 +1012,6 @@ init 999 python in maica:
10211012

10221013
def refresh_accessories(self):
10231014
self.accessory_data = {
1024-
"__none__": (None, None),
10251015
"wear|玩家挑选": ("wear", "mas_pick_a_clothes"),
10261016
}
10271017
source = "store.mas_selspr.ACS_SEL_MAP"
@@ -1032,7 +1022,10 @@ init 999 python in maica:
10321022
if not self.outfit_has_and_unlocked(key):
10331023
continue
10341024
display_name = accessory_map[key].display_name
1035-
reason = mtrigger_item_error(display_name)
1025+
reason = (
1026+
mtrigger_item_error(display_name)
1027+
or _reserved_mtrigger_item_error(display_name)
1028+
)
10361029
if reason is not None:
10371030
log_invalid_mtrigger(
10381031
"accessory",
@@ -1083,7 +1076,10 @@ init 999 python in maica:
10831076
if key not in store.mas_selspr.ACS_SEL_MAP:
10841077
continue
10851078
display_name = store.mas_selspr.ACS_SEL_MAP[key].display_name
1086-
reason = mtrigger_item_error(display_name)
1079+
reason = (
1080+
mtrigger_item_error(display_name)
1081+
or _reserved_mtrigger_item_error(display_name)
1082+
)
10871083
if reason is not None:
10881084
log_invalid_mtrigger(
10891085
"accessory",
@@ -1133,11 +1129,11 @@ init 999 python in maica:
11331129
def accessory_callback(self, choice):
11341130
if choice is None:
11351131
return
1132+
if choice == "__none__":
1133+
return
11361134
if not _valid_mtrigger_selection(self.accessory_data, "accessory", choice):
11371135
return
11381136
action, accessory = self.accessory_data[choice]
1139-
if choice == "__none__":
1140-
return
11411137
if action == "unwear":
11421138
return store.renpy.call("mtrigger_unwear_acs", accessory)
11431139
return store.renpy.call("mtrigger_change_acs", accessory)

‎game/python-packages/maica_mtrigger.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ def triggered(self, data=None):
628628
value = data.get(self.template.datakey) if self.template.datakey else None
629629
if value is None and self.template.name == common_affection_template.name:
630630
value = data.get("affection")
631-
if self.perf_suggestion and "suggestion" in data:
631+
if self.perf_suggestion and data.get("suggestion") is not None:
632632
return self.callback(data.get("suggestion"))
633633
if value is None and self.template.exprop.suggestion and "suggestion" in data:
634634
value = data.get("suggestion")

‎tests/test_mtrigger_dialogue_flow.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def mas_play_song(song):
5858
}
5959
exec(compile(class_source, "trigger.rpy:MusicTrigger", "exec"), namespace)
6060
trigger = object.__new__(namespace["MusicTrigger"])
61-
trigger.musics = ["__none__"] + [choice[0] for choice in music_choices]
61+
trigger.musics = [choice[0] for choice in music_choices]
6262
return trigger, store, invalid
6363

6464

‎tests/test_mtrigger_none_compat.py‎

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import ast
2+
import sys
3+
from pathlib import Path
4+
from types import ModuleType, SimpleNamespace
5+
6+
import pytest
7+
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
PACKAGE_ROOT = ROOT / "game" / "python-packages"
11+
if str(PACKAGE_ROOT) not in sys.path:
12+
sys.path.insert(0, str(PACKAGE_ROOT))
13+
14+
import maica_mtrigger
15+
16+
17+
def _builtin_trigger_runtime(monkeypatch, web_music=False):
18+
source_path = ROOT / "game" / "Submods" / "MAICA_ChatSubmod" / "trigger.rpy"
19+
source = source_path.read_text(encoding="utf-8").split(
20+
"init 999 python in maica:", 1
21+
)[1]
22+
source = "\n".join(
23+
line[4:] if line.startswith(" ") else line for line in source.splitlines()
24+
)
25+
tree = ast.parse(source, filename=str(source_path))
26+
names = {
27+
"_reserved_mtrigger_item_error",
28+
"_add_mtrigger_item",
29+
"_valid_mtrigger_selection",
30+
"get_unlocked_games",
31+
"minigame_callback",
32+
"ClothesTrigger",
33+
"HairTrigger",
34+
"MinigameTrigger",
35+
"WeatherTrigger",
36+
"MusicTrigger",
37+
"AccessoryTrigger",
38+
}
39+
tree.body = [
40+
node for node in tree.body
41+
if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names
42+
]
43+
44+
calls = []
45+
invalid = []
46+
store = ModuleType("store")
47+
store.__path__ = []
48+
store.renpy = SimpleNamespace(call=lambda *args: calls.append(args))
49+
store.mas_submod_utils = SimpleNamespace(
50+
isSubmodInstalled=lambda name: web_music and name == "Netease Music"
51+
)
52+
store.songs = SimpleNamespace(
53+
music_choices=[("Song", "song.ogg"), ("__none__", "legacy.ogg")],
54+
getPlayingMusicName=lambda: "Song",
55+
)
56+
store.mas_play_song = lambda song: calls.append(("play_song", song))
57+
entry = lambda name: SimpleNamespace(display_name=name, unlocked=True)
58+
store.mas_selspr = SimpleNamespace(
59+
CLOTH_SEL_MAP={"outfit": entry("Outfit"), "legacy": entry("__none__")},
60+
HAIR_SEL_MAP={"hair": entry("Hair"), "legacy": entry("__none__")},
61+
ACS_SEL_MAP={"hat": entry("Hat"), "legacy": entry("__none__")},
62+
)
63+
worn_hat = SimpleNamespace(name="hat")
64+
store.monika_chr = SimpleNamespace(
65+
clothes=SimpleNamespace(name="outfit"),
66+
hair=SimpleNamespace(name="hair"),
67+
get_acs=lambda: [worn_hat, SimpleNamespace(name="legacy")],
68+
)
69+
store.mas_games = SimpleNamespace(game_db={
70+
"valid": SimpleNamespace(prompt="Chess", eventlabel="game_chess"),
71+
"legacy": SimpleNamespace(prompt="__none__", eventlabel="game_legacy"),
72+
})
73+
store.mas_isGameUnlocked = lambda name: name != "Hangman" and name != "上吊小人"
74+
store.mas_isMoniAff = lambda higher: True
75+
sunny = SimpleNamespace(prompt="Sunny", unlocked=True)
76+
store.mas_weather_def = sunny
77+
store.mas_current_weather = sunny
78+
store.mas_weather = ModuleType("store.mas_weather")
79+
store.mas_weather.WEATHER_MAP = {
80+
"def": sunny,
81+
"legacy": SimpleNamespace(prompt="__none__", unlocked=True),
82+
"rain": SimpleNamespace(prompt="Rain", unlocked=True),
83+
}
84+
monkeypatch.setitem(sys.modules, "store", store)
85+
monkeypatch.setitem(sys.modules, "store.mas_weather", store.mas_weather)
86+
87+
namespace = dict(vars(maica_mtrigger))
88+
namespace.update({
89+
"store": store,
90+
"_": lambda text: text,
91+
"log_invalid_mtrigger": lambda *args, **kwargs: invalid.append((args, kwargs)),
92+
})
93+
exec(compile(tree, str(source_path), "exec"), namespace)
94+
namespace["unlocked_games_dict"] = namespace["get_unlocked_games"]()
95+
96+
manager = maica_mtrigger.MTriggerManager()
97+
for name, args in (
98+
("ClothesTrigger", (maica_mtrigger.common_switch_template, "clothes")),
99+
("HairTrigger", (maica_mtrigger.common_switch_template, "hair")),
100+
("MinigameTrigger", ()),
101+
("WeatherTrigger", ()),
102+
("MusicTrigger", ()),
103+
("AccessoryTrigger", ()),
104+
):
105+
manager.add_trigger(namespace[name](*args))
106+
return SimpleNamespace(
107+
manager=manager, store=store, calls=calls, invalid=invalid, worn_hat=worn_hat
108+
)
109+
110+
111+
@pytest.fixture
112+
def runtime(monkeypatch):
113+
return _builtin_trigger_runtime(monkeypatch)
114+
115+
116+
def test_builtin_switches_exclude_reserved_names_from_built_payload(runtime):
117+
built = {item["name"]: item["exprop"]["item_list"]
118+
for item in runtime.manager.build_data()}
119+
120+
assert set(built) == {"clothes", "hair", "minigame", "weather", "music", "accessory"}
121+
assert all("__none__" not in choices for choices in built.values())
122+
assert "wear|__none__" not in built["accessory"]
123+
assert "unwear|__none__" not in built["accessory"]
124+
assert {"玩家挑选", "Outfit"} <= set(built["clothes"])
125+
assert {"玩家挑选", "Hair"} <= set(built["hair"])
126+
assert {"玩家自行选择", "Chess"} <= set(built["minigame"])
127+
assert {"Sunny", "Rain"} <= set(built["weather"])
128+
assert {"玩家自行选择", "Song", "停止/静音"} <= set(built["music"])
129+
assert {"wear|玩家挑选", "wear|Hat", "unwear|Hat"} <= set(built["accessory"])
130+
rejected = {args[0] for args, kwargs in runtime.invalid
131+
if args[1] == "__none__" and "reserved" in kwargs.get("reason", "")}
132+
assert rejected == set(built)
133+
134+
135+
@pytest.mark.parametrize("name,choice,expected", [
136+
("clothes", "__none__", ("mtrigger_change_clothes", "mas_pick_a_clothes")),
137+
("clothes", "玩家挑选", ("mtrigger_change_clothes", "mas_pick_a_clothes")),
138+
("hair", "__none__", ("mtrigger_change_hair", "mas_pick_a_clothes")),
139+
("hair", "玩家挑选", ("mtrigger_change_hair", "mas_pick_a_clothes")),
140+
("minigame", "__none__", ("mtrigger_minigame", "mas_pick_a_game")),
141+
("minigame", "玩家自行选择", ("mtrigger_minigame", "mas_pick_a_game")),
142+
("weather", "__none__", None),
143+
("music", "__none__", None),
144+
("accessory", "__none__", None),
145+
("accessory", "wear|Hat", ("mtrigger_change_acs", "hat")),
146+
])
147+
def test_legacy_and_readable_choices_keep_their_callback_behavior(runtime, name, choice, expected):
148+
runtime.manager.triggered(name, {"choice": choice})
149+
runtime.manager.run_trigger()
150+
assert runtime.calls == ([] if expected is None else [expected])
151+
152+
153+
@pytest.mark.parametrize("name", ["clothes", "hair", "minigame", "weather", "music", "accessory"])
154+
def test_null_choice_does_not_perform_an_action(runtime, name):
155+
runtime.manager.triggered(name, {"choice": None})
156+
runtime.manager.run_trigger()
157+
assert runtime.calls == []
158+
159+
160+
def test_accessory_unwear_remains_available(runtime):
161+
runtime.manager.triggered("accessory", {"choice": "unwear|Hat"})
162+
runtime.manager.run_trigger()
163+
assert runtime.calls == [("mtrigger_unwear_acs", runtime.worn_hat)]
164+
165+
166+
def test_empty_weather_is_skipped_and_returns_when_data_recovers(runtime):
167+
legacy = SimpleNamespace(prompt="__none__", unlocked=True)
168+
runtime.store.mas_weather_def = legacy
169+
runtime.store.mas_weather.WEATHER_MAP = {"legacy": legacy}
170+
171+
assert "weather" not in {item["name"] for item in runtime.manager.build_data()}
172+
assert runtime.manager.trigger_status("weather") is True
173+
174+
sunny = SimpleNamespace(prompt="Sunny", unlocked=True)
175+
runtime.store.mas_weather_def = sunny
176+
runtime.store.mas_weather.WEATHER_MAP = {"def": sunny}
177+
built = {item["name"]: item for item in runtime.manager.build_data()}
178+
assert built["weather"]["exprop"]["item_list"] == ["Sunny"]
179+
runtime.manager.triggered("weather", {"choice": "Sunny"})
180+
runtime.manager.run_trigger()
181+
assert runtime.calls == [("mtrigger_weather", sunny)]
182+
183+
184+
@pytest.mark.parametrize("payload,expected_label,expected_selection", [
185+
({"choice": "Song", "suggestion": None}, "mtrigger_music_auto", "Song"),
186+
({"choice": "Song"}, "mtrigger_music_auto", "Song"),
187+
({"choice": "Song", "suggestion": "Online song"}, "mtrigger_neteasemusic_search", "Online song"),
188+
({"choice": None, "suggestion": "Online song"}, "mtrigger_neteasemusic_search", "Online song"),
189+
({"choice": None, "suggestion": None}, None, None),
190+
({"choice": "__none__", "suggestion": None}, None, None),
191+
])
192+
def test_web_music_uses_choice_when_suggestion_is_null(
193+
monkeypatch, payload, expected_label, expected_selection
194+
):
195+
runtime = _builtin_trigger_runtime(monkeypatch, web_music=True)
196+
built = {item["name"]: item for item in runtime.manager.build_data()}
197+
assert built["music"]["exprop"]["suggestion"] is True
198+
runtime.manager.triggered("music", payload)
199+
runtime.manager.run_trigger()
200+
201+
if expected_label is None:
202+
assert runtime.calls == []
203+
else:
204+
assert len(runtime.calls) == 1
205+
assert runtime.calls[0][0] == expected_label
206+
assert runtime.calls[0][-1] == expected_selection
207+
208+
209+
@pytest.mark.parametrize("prefer_suggestion,payload,expected", [
210+
(True, {"choice": "Song", "suggestion": None}, "Song"),
211+
(True, {"choice": "Song", "suggestion": ""}, ""),
212+
(True, {"choice": "Song", "suggestion": False}, False),
213+
(False, {"choice": "Song", "suggestion": "Other"}, "Song"),
214+
(False, {"choice": None, "suggestion": "Other"}, "Other"),
215+
])
216+
def test_suggestion_priority_preserves_non_null_values(prefer_suggestion, payload, expected):
217+
values = []
218+
trigger = maica_mtrigger.MTriggerBase(
219+
maica_mtrigger.common_switch_template,
220+
"switch",
221+
callback=values.append,
222+
exprop=maica_mtrigger.MTriggerExprop(
223+
item_name_zh="music", item_list=["Song"], suggestion=True
224+
),
225+
perf_suggestion=prefer_suggestion,
226+
)
227+
trigger.triggered(payload)
228+
assert values == [expected]

0 commit comments

Comments
 (0)