Core: Introduce 'Hint Priority' concept (#3506)
* Introduce 'Hint Priority' concept * fix error when sorting hints while not connected * fix 'found' -> 'status' kivy stuff * remove extraneous warning this warning fired if you clicked to select or toggle priority of any hint, as you weren't clicking on the header... * skip scanning individual header widgets when not clicking on the header * update hints on disconnection * minor cleanup * minor fixes/cleanup * fix: hints not updating properly for receiving player * update re: review * 'type() is' -> 'isinstance()' * cleanup, re: Jouramie's review * Change 'priority' to 'status', add 'Unspecified' and 'Avoid' statuses, update colors * cleanup * move dicts out of functions * fix: new hints being returned when hint already exists * fix: show `Found` properly when hinting already-found hints * import `Hint` and `HintStatus` directly from `NetUtils` * Default any hinted `Trap` item to be classified as `Avoid` by default * add some sanity checks * re: Vi's feedback * move dict out of function * Update kvui.py * remove unneeded dismiss message * allow lclick to drop hint status dropdown * underline hint statuses to indicate clickability * only underline clickable statuses * Update kvui.py * Update kvui.py --------- Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
parent
b972e8c071
commit
b783eab1e8
|
@ -23,7 +23,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
|
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||||
from Utils import Version, stream_input, async_start
|
from Utils import Version, stream_input, async_start
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
import os
|
import os
|
||||||
|
@ -412,6 +412,7 @@ class CommonContext:
|
||||||
await self.server.socket.close()
|
await self.server.socket.close()
|
||||||
if self.server_task is not None:
|
if self.server_task is not None:
|
||||||
await self.server_task
|
await self.server_task
|
||||||
|
self.ui.update_hints()
|
||||||
|
|
||||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||||
""" `msgs` JSON serializable """
|
""" `msgs` JSON serializable """
|
||||||
|
@ -551,7 +552,14 @@ class CommonContext:
|
||||||
await self.ui_task
|
await self.ui_task
|
||||||
if self.input_task:
|
if self.input_task:
|
||||||
self.input_task.cancel()
|
self.input_task.cancel()
|
||||||
|
|
||||||
|
# Hints
|
||||||
|
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
|
||||||
|
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
|
||||||
|
if status is not None:
|
||||||
|
msg["status"] = status
|
||||||
|
async_start(self.send_msgs([msg]), name="update_hint")
|
||||||
|
|
||||||
# DataPackage
|
# DataPackage
|
||||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||||
remote_date_package_versions: typing.Dict[str, int],
|
remote_date_package_versions: typing.Dict[str, int],
|
||||||
|
|
2
Main.py
2
Main.py
|
@ -276,7 +276,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||||
def precollect_hint(location):
|
def precollect_hint(location):
|
||||||
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
||||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||||
location.item.code, False, entrance, location.item.flags)
|
location.item.code, False, entrance, location.item.flags, False)
|
||||||
precollected_hints[location.player].add(hint)
|
precollected_hints[location.player].add(hint)
|
||||||
if location.item.player not in multiworld.groups:
|
if location.item.player not in multiworld.groups:
|
||||||
precollected_hints[location.item.player].add(hint)
|
precollected_hints[location.item.player].add(hint)
|
||||||
|
|
174
MultiServer.py
174
MultiServer.py
|
@ -41,7 +41,8 @@ import NetUtils
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||||
SlotType, LocationStore
|
SlotType, LocationStore, Hint, HintStatus
|
||||||
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
min_client_version = Version(0, 1, 6)
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
@ -228,7 +229,7 @@ class Context:
|
||||||
self.hint_cost = hint_cost
|
self.hint_cost = hint_cost
|
||||||
self.location_check_points = location_check_points
|
self.location_check_points = location_check_points
|
||||||
self.hints_used = collections.defaultdict(int)
|
self.hints_used = collections.defaultdict(int)
|
||||||
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
|
self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set)
|
||||||
self.release_mode: str = release_mode
|
self.release_mode: str = release_mode
|
||||||
self.remaining_mode: str = remaining_mode
|
self.remaining_mode: str = remaining_mode
|
||||||
self.collect_mode: str = collect_mode
|
self.collect_mode: str = collect_mode
|
||||||
|
@ -656,13 +657,29 @@ class Context:
|
||||||
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None,
|
||||||
|
changed: typing.Optional[typing.Set[team_slot]] = None) -> None:
|
||||||
|
"""Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot
|
||||||
|
will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot)
|
||||||
|
pair that has at least one hint modified will be added to the set.
|
||||||
|
"""
|
||||||
for hint_team, hint_slot in self.hints:
|
for hint_team, hint_slot in self.hints:
|
||||||
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
|
if team != hint_team and team is not None:
|
||||||
self.hints[hint_team, hint_slot] = {
|
continue # Check specified team only, all if team is None
|
||||||
hint.re_check(self, hint_team) for hint in
|
if slot != hint_slot and slot is not None:
|
||||||
self.hints[hint_team, hint_slot]
|
continue # Check specified slot only, all if slot is None
|
||||||
}
|
new_hints: typing.Set[Hint] = set()
|
||||||
|
for hint in self.hints[hint_team, hint_slot]:
|
||||||
|
new_hint = hint.re_check(self, hint_team)
|
||||||
|
new_hints.add(new_hint)
|
||||||
|
if hint == new_hint:
|
||||||
|
continue
|
||||||
|
for player in self.slot_set(hint.receiving_player) | {hint.finding_player}:
|
||||||
|
if changed is not None:
|
||||||
|
changed.add((hint_team,player))
|
||||||
|
if slot is not None and slot != player:
|
||||||
|
self.replace_hint(hint_team, player, hint, new_hint)
|
||||||
|
self.hints[hint_team, hint_slot] = new_hints
|
||||||
|
|
||||||
def get_rechecked_hints(self, team: int, slot: int):
|
def get_rechecked_hints(self, team: int, slot: int):
|
||||||
self.recheck_hints(team, slot)
|
self.recheck_hints(team, slot)
|
||||||
|
@ -711,7 +728,7 @@ class Context:
|
||||||
else:
|
else:
|
||||||
return self.player_names[team, slot]
|
return self.player_names[team, slot]
|
||||||
|
|
||||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
|
def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False,
|
||||||
recipients: typing.Sequence[int] = None):
|
recipients: typing.Sequence[int] = None):
|
||||||
"""Send and remember hints."""
|
"""Send and remember hints."""
|
||||||
if only_new:
|
if only_new:
|
||||||
|
@ -749,6 +766,17 @@ class Context:
|
||||||
for client in clients:
|
for client in clients:
|
||||||
async_start(self.send_msgs(client, client_hints))
|
async_start(self.send_msgs(client, client_hints))
|
||||||
|
|
||||||
|
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
|
||||||
|
for hint in self.hints[team, finding_player]:
|
||||||
|
if hint.location == seeked_location:
|
||||||
|
return hint
|
||||||
|
return None
|
||||||
|
|
||||||
|
def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None:
|
||||||
|
if old_hint in self.hints[team, slot]:
|
||||||
|
self.hints[team, slot].remove(old_hint)
|
||||||
|
self.hints[team, slot].add(new_hint)
|
||||||
|
|
||||||
# "events"
|
# "events"
|
||||||
|
|
||||||
def on_goal_achieved(self, client: Client):
|
def on_goal_achieved(self, client: Client):
|
||||||
|
@ -1050,14 +1078,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||||
"hint_points": get_slot_points(ctx, team, slot),
|
"hint_points": get_slot_points(ctx, team, slot),
|
||||||
"checked_locations": new_locations, # send back new checks only
|
"checked_locations": new_locations, # send back new checks only
|
||||||
}])
|
}])
|
||||||
old_hints = ctx.hints[team, slot].copy()
|
updated_slots: typing.Set[tuple[int, int]] = set()
|
||||||
ctx.recheck_hints(team, slot)
|
ctx.recheck_hints(team, slot, updated_slots)
|
||||||
if old_hints != ctx.hints[team, slot]:
|
for hint_team, hint_slot in updated_slots:
|
||||||
ctx.on_changed_hints(team, slot)
|
ctx.on_changed_hints(hint_team, hint_slot)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
|
|
||||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
|
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \
|
||||||
|
-> typing.List[Hint]:
|
||||||
hints = []
|
hints = []
|
||||||
slots: typing.Set[int] = {slot}
|
slots: typing.Set[int] = {slot}
|
||||||
for group_id, group in ctx.groups.items():
|
for group_id, group in ctx.groups.items():
|
||||||
|
@ -1067,31 +1096,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
||||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||||
for finding_player, location_id, item_id, receiving_player, item_flags \
|
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||||
in ctx.locations.find_item(slots, seeked_item_id):
|
in ctx.locations.find_item(slots, seeked_item_id):
|
||||||
found = location_id in ctx.location_checks[team, finding_player]
|
prev_hint = ctx.get_hint(team, slot, location_id)
|
||||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
if prev_hint:
|
||||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
hints.append(prev_hint)
|
||||||
item_flags))
|
else:
|
||||||
|
found = location_id in ctx.location_checks[team, finding_player]
|
||||||
|
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||||
|
new_status = auto_status
|
||||||
|
if found:
|
||||||
|
new_status = HintStatus.HINT_FOUND
|
||||||
|
elif item_flags & ItemClassification.trap:
|
||||||
|
new_status = HintStatus.HINT_AVOID
|
||||||
|
hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||||
|
item_flags, new_status))
|
||||||
|
|
||||||
return hints
|
return hints
|
||||||
|
|
||||||
|
|
||||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \
|
||||||
|
-> typing.List[Hint]:
|
||||||
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
||||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status)
|
||||||
|
|
||||||
|
|
||||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]:
|
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \
|
||||||
|
-> typing.List[Hint]:
|
||||||
|
prev_hint = ctx.get_hint(team, slot, seeked_location)
|
||||||
|
if prev_hint:
|
||||||
|
return [prev_hint]
|
||||||
result = ctx.locations[slot].get(seeked_location, (None, None, None))
|
result = ctx.locations[slot].get(seeked_location, (None, None, None))
|
||||||
if any(result):
|
if any(result):
|
||||||
item_id, receiving_player, item_flags = result
|
item_id, receiving_player, item_flags = result
|
||||||
|
|
||||||
found = seeked_location in ctx.location_checks[team, slot]
|
found = seeked_location in ctx.location_checks[team, slot]
|
||||||
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||||
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)]
|
new_status = auto_status
|
||||||
|
if found:
|
||||||
|
new_status = HintStatus.HINT_FOUND
|
||||||
|
elif item_flags & ItemClassification.trap:
|
||||||
|
new_status = HintStatus.HINT_AVOID
|
||||||
|
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags,
|
||||||
|
new_status)]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
status_names: typing.Dict[HintStatus, str] = {
|
||||||
|
HintStatus.HINT_FOUND: "(found)",
|
||||||
|
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "(no priority)",
|
||||||
|
HintStatus.HINT_AVOID: "(avoid)",
|
||||||
|
HintStatus.HINT_PRIORITY: "(priority)",
|
||||||
|
}
|
||||||
|
def format_hint(ctx: Context, team: int, hint: Hint) -> str:
|
||||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||||
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
|
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
|
||||||
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
|
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
|
||||||
|
@ -1099,7 +1155,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||||
|
|
||||||
if hint.entrance:
|
if hint.entrance:
|
||||||
text += f" at {hint.entrance}"
|
text += f" at {hint.entrance}"
|
||||||
return text + (". (found)" if hint.found else ".")
|
|
||||||
|
return text + ". " + status_names.get(hint.status, "(unknown)")
|
||||||
|
|
||||||
|
|
||||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||||
|
@ -1503,7 +1560,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
|
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
|
||||||
points_available = get_client_points(self.ctx, self.client)
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||||
|
auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY
|
||||||
if not input_text:
|
if not input_text:
|
||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
self.ctx.hints[self.client.team, self.client.slot]}
|
self.ctx.hints[self.client.team, self.client.slot]}
|
||||||
|
@ -1529,9 +1586,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
hints = []
|
||||||
elif not for_location:
|
elif not for_location:
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||||
else:
|
else:
|
||||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
|
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
game = self.ctx.games[self.client.slot]
|
game = self.ctx.games[self.client.slot]
|
||||||
|
@ -1551,16 +1608,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
hints = []
|
hints = []
|
||||||
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||||
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status))
|
||||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||||
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
||||||
hints = []
|
hints = []
|
||||||
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
||||||
if loc_name in self.ctx.location_names_for_game(game):
|
if loc_name in self.ctx.location_names_for_game(game):
|
||||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
|
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status))
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.output(response)
|
self.output(response)
|
||||||
|
@ -1832,13 +1889,51 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
|
|
||||||
target_item, target_player, flags = ctx.locations[client.slot][location]
|
target_item, target_player, flags = ctx.locations[client.slot][location]
|
||||||
if create_as_hint:
|
if create_as_hint:
|
||||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location,
|
||||||
|
HintStatus.HINT_UNSPECIFIED))
|
||||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||||
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
||||||
if locs and create_as_hint:
|
if locs and create_as_hint:
|
||||||
ctx.save()
|
ctx.save()
|
||||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||||
|
|
||||||
|
elif cmd == 'UpdateHint':
|
||||||
|
location = args["location"]
|
||||||
|
player = args["player"]
|
||||||
|
status = args["status"]
|
||||||
|
if not isinstance(player, int) or not isinstance(location, int) \
|
||||||
|
or (status is not None and not isinstance(status, int)):
|
||||||
|
await ctx.send_msgs(client,
|
||||||
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint',
|
||||||
|
"original_cmd": cmd}])
|
||||||
|
return
|
||||||
|
hint = ctx.get_hint(client.team, player, location)
|
||||||
|
if not hint:
|
||||||
|
return # Ignored safely
|
||||||
|
if hint.receiving_player != client.slot:
|
||||||
|
await ctx.send_msgs(client,
|
||||||
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission',
|
||||||
|
"original_cmd": cmd}])
|
||||||
|
return
|
||||||
|
new_hint = hint
|
||||||
|
if status is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
status = HintStatus(status)
|
||||||
|
except ValueError:
|
||||||
|
await ctx.send_msgs(client,
|
||||||
|
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||||
|
"text": 'UpdateHint: Invalid Status', "original_cmd": cmd}])
|
||||||
|
return
|
||||||
|
new_hint = new_hint.re_prioritize(ctx, status)
|
||||||
|
if hint == new_hint:
|
||||||
|
return
|
||||||
|
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
||||||
|
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
||||||
|
ctx.save()
|
||||||
|
ctx.on_changed_hints(client.team, hint.finding_player)
|
||||||
|
ctx.on_changed_hints(client.team, hint.receiving_player)
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
update_client_status(ctx, client, args["status"])
|
update_client_status(ctx, client, args["status"])
|
||||||
|
|
||||||
|
@ -2143,9 +2238,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
hints = []
|
hints = []
|
||||||
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
||||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY))
|
||||||
else: # item name or id
|
else: # item name or id
|
||||||
hints = collect_hints(self.ctx, team, slot, item)
|
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
|
||||||
|
|
||||||
if hints:
|
if hints:
|
||||||
self.ctx.notify_hints(team, hints)
|
self.ctx.notify_hints(team, hints)
|
||||||
|
@ -2179,14 +2274,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
|
|
||||||
if usable:
|
if usable:
|
||||||
if isinstance(location, int):
|
if isinstance(location, int):
|
||||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
hints = collect_hint_location_id(self.ctx, team, slot, location,
|
||||||
|
HintStatus.HINT_UNSPECIFIED)
|
||||||
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
||||||
hints = []
|
hints = []
|
||||||
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
||||||
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
||||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
|
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group,
|
||||||
|
HintStatus.HINT_UNSPECIFIED))
|
||||||
else:
|
else:
|
||||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
hints = collect_hint_location_name(self.ctx, team, slot, location,
|
||||||
|
HintStatus.HINT_UNSPECIFIED)
|
||||||
if hints:
|
if hints:
|
||||||
self.ctx.notify_hints(team, hints)
|
self.ctx.notify_hints(team, hints)
|
||||||
else:
|
else:
|
||||||
|
|
41
NetUtils.py
41
NetUtils.py
|
@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum):
|
||||||
CLIENT_GOAL = 30
|
CLIENT_GOAL = 30
|
||||||
|
|
||||||
|
|
||||||
|
class HintStatus(enum.IntEnum):
|
||||||
|
HINT_FOUND = 0
|
||||||
|
HINT_UNSPECIFIED = 1
|
||||||
|
HINT_NO_PRIORITY = 10
|
||||||
|
HINT_AVOID = 20
|
||||||
|
HINT_PRIORITY = 30
|
||||||
|
|
||||||
|
|
||||||
class SlotType(ByValue, enum.IntFlag):
|
class SlotType(ByValue, enum.IntFlag):
|
||||||
spectator = 0b00
|
spectator = 0b00
|
||||||
player = 0b01
|
player = 0b01
|
||||||
|
@ -297,6 +305,20 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs)
|
||||||
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||||
|
|
||||||
|
|
||||||
|
status_names: typing.Dict[HintStatus, str] = {
|
||||||
|
HintStatus.HINT_FOUND: "(found)",
|
||||||
|
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "(no priority)",
|
||||||
|
HintStatus.HINT_AVOID: "(avoid)",
|
||||||
|
HintStatus.HINT_PRIORITY: "(priority)",
|
||||||
|
}
|
||||||
|
status_colors: typing.Dict[HintStatus, str] = {
|
||||||
|
HintStatus.HINT_FOUND: "green",
|
||||||
|
HintStatus.HINT_UNSPECIFIED: "white",
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "slateblue",
|
||||||
|
HintStatus.HINT_AVOID: "salmon",
|
||||||
|
HintStatus.HINT_PRIORITY: "plum",
|
||||||
|
}
|
||||||
class Hint(typing.NamedTuple):
|
class Hint(typing.NamedTuple):
|
||||||
receiving_player: int
|
receiving_player: int
|
||||||
finding_player: int
|
finding_player: int
|
||||||
|
@ -305,14 +327,21 @@ class Hint(typing.NamedTuple):
|
||||||
found: bool
|
found: bool
|
||||||
entrance: str = ""
|
entrance: str = ""
|
||||||
item_flags: int = 0
|
item_flags: int = 0
|
||||||
|
status: HintStatus = HintStatus.HINT_UNSPECIFIED
|
||||||
|
|
||||||
def re_check(self, ctx, team) -> Hint:
|
def re_check(self, ctx, team) -> Hint:
|
||||||
if self.found:
|
if self.found and self.status == HintStatus.HINT_FOUND:
|
||||||
return self
|
return self
|
||||||
found = self.location in ctx.location_checks[team, self.finding_player]
|
found = self.location in ctx.location_checks[team, self.finding_player]
|
||||||
if found:
|
if found:
|
||||||
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
|
return self._replace(found=found, status=HintStatus.HINT_FOUND)
|
||||||
self.item_flags)
|
return self
|
||||||
|
|
||||||
|
def re_prioritize(self, ctx, status: HintStatus) -> Hint:
|
||||||
|
if self.found and status != HintStatus.HINT_FOUND:
|
||||||
|
status = HintStatus.HINT_FOUND
|
||||||
|
if status != self.status:
|
||||||
|
return self._replace(status=status)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
|
@ -334,10 +363,8 @@ class Hint(typing.NamedTuple):
|
||||||
else:
|
else:
|
||||||
add_json_text(parts, "'s World")
|
add_json_text(parts, "'s World")
|
||||||
add_json_text(parts, ". ")
|
add_json_text(parts, ". ")
|
||||||
if self.found:
|
add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color",
|
||||||
add_json_text(parts, "(found)", type="color", color="green")
|
color=status_colors.get(self.status, "red"))
|
||||||
else:
|
|
||||||
add_json_text(parts, "(not found)", type="color", color="red")
|
|
||||||
|
|
||||||
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||||
"receiving": self.receiving_player,
|
"receiving": self.receiving_player,
|
||||||
|
|
3
Utils.py
3
Utils.py
|
@ -421,7 +421,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
return getattr(builtins, name)
|
return getattr(builtins, name)
|
||||||
# used by MultiServer -> savegame/multidata
|
# used by MultiServer -> savegame/multidata
|
||||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
|
||||||
|
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||||
return getattr(self.net_utils_module, name)
|
return getattr(self.net_utils_module, name)
|
||||||
# Options and Plando are unpickled by WebHost -> Generate
|
# Options and Plando are unpickled by WebHost -> Generate
|
||||||
if module == "worlds.generic" and name == "PlandoItem":
|
if module == "worlds.generic" and name == "PlandoItem":
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
finding_text: "Finding Player"
|
finding_text: "Finding Player"
|
||||||
location_text: "Location"
|
location_text: "Location"
|
||||||
entrance_text: "Entrance"
|
entrance_text: "Entrance"
|
||||||
found_text: "Found?"
|
status_text: "Status"
|
||||||
TooltipLabel:
|
TooltipLabel:
|
||||||
id: receiving
|
id: receiving
|
||||||
sort_key: 'receiving'
|
sort_key: 'receiving'
|
||||||
|
@ -96,9 +96,9 @@
|
||||||
valign: 'center'
|
valign: 'center'
|
||||||
pos_hint: {"center_y": 0.5}
|
pos_hint: {"center_y": 0.5}
|
||||||
TooltipLabel:
|
TooltipLabel:
|
||||||
id: found
|
id: status
|
||||||
sort_key: 'found'
|
sort_key: 'status'
|
||||||
text: root.found_text
|
text: root.status_text
|
||||||
halign: 'center'
|
halign: 'center'
|
||||||
valign: 'center'
|
valign: 'center'
|
||||||
pos_hint: {"center_y": 0.5}
|
pos_hint: {"center_y": 0.5}
|
||||||
|
|
|
@ -272,6 +272,7 @@ These packets are sent purely from client to server. They are not accepted by cl
|
||||||
* [Sync](#Sync)
|
* [Sync](#Sync)
|
||||||
* [LocationChecks](#LocationChecks)
|
* [LocationChecks](#LocationChecks)
|
||||||
* [LocationScouts](#LocationScouts)
|
* [LocationScouts](#LocationScouts)
|
||||||
|
* [UpdateHint](#UpdateHint)
|
||||||
* [StatusUpdate](#StatusUpdate)
|
* [StatusUpdate](#StatusUpdate)
|
||||||
* [Say](#Say)
|
* [Say](#Say)
|
||||||
* [GetDataPackage](#GetDataPackage)
|
* [GetDataPackage](#GetDataPackage)
|
||||||
|
@ -342,6 +343,29 @@ This is useful in cases where an item appears in the game world, such as 'ledge
|
||||||
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||||
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
||||||
|
|
||||||
|
### UpdateHint
|
||||||
|
Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails.
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
| Name | Type | Notes |
|
||||||
|
| ---- | ---- | ----- |
|
||||||
|
| player | int | The ID of the player whose location is being hinted for. |
|
||||||
|
| location | int | The ID of the location to update the hint for. If no hint exists for this location, the packet is ignored. |
|
||||||
|
| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. |
|
||||||
|
|
||||||
|
#### HintStatus
|
||||||
|
An enumeration containing the possible hint states.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import enum
|
||||||
|
class HintStatus(enum.IntEnum):
|
||||||
|
HINT_FOUND = 0
|
||||||
|
HINT_UNSPECIFIED = 1
|
||||||
|
HINT_NO_PRIORITY = 10
|
||||||
|
HINT_AVOID = 20
|
||||||
|
HINT_PRIORITY = 30
|
||||||
|
```
|
||||||
|
|
||||||
### StatusUpdate
|
### StatusUpdate
|
||||||
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
|
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
|
||||||
|
|
||||||
|
|
96
kvui.py
96
kvui.py
|
@ -52,6 +52,7 @@ from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.floatlayout import FloatLayout
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
from kivy.uix.progressbar import ProgressBar
|
from kivy.uix.progressbar import ProgressBar
|
||||||
|
from kivy.uix.dropdown import DropDown
|
||||||
from kivy.utils import escape_markup
|
from kivy.utils import escape_markup
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||||
|
@ -63,7 +64,7 @@ from kivy.uix.popup import Popup
|
||||||
|
|
||||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||||
|
|
||||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
|
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
|
||||||
from Utils import async_start, get_input_text_from_response
|
from Utils import async_start, get_input_text_from_response
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
|
@ -300,11 +301,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||||
selected = BooleanProperty(False)
|
selected = BooleanProperty(False)
|
||||||
striped = BooleanProperty(False)
|
striped = BooleanProperty(False)
|
||||||
index = None
|
index = None
|
||||||
|
dropdown: DropDown
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HintLabel, self).__init__()
|
super(HintLabel, self).__init__()
|
||||||
|
@ -313,10 +314,32 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||||
self.finding_text = ""
|
self.finding_text = ""
|
||||||
self.location_text = ""
|
self.location_text = ""
|
||||||
self.entrance_text = ""
|
self.entrance_text = ""
|
||||||
self.found_text = ""
|
self.status_text = ""
|
||||||
|
self.hint = {}
|
||||||
for child in self.children:
|
for child in self.children:
|
||||||
child.bind(texture_size=self.set_height)
|
child.bind(texture_size=self.set_height)
|
||||||
|
|
||||||
|
|
||||||
|
ctx = App.get_running_app().ctx
|
||||||
|
self.dropdown = DropDown()
|
||||||
|
|
||||||
|
def set_value(button):
|
||||||
|
self.dropdown.select(button.status)
|
||||||
|
|
||||||
|
def select(instance, data):
|
||||||
|
ctx.update_hint(self.hint["location"],
|
||||||
|
self.hint["finding_player"],
|
||||||
|
data)
|
||||||
|
|
||||||
|
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||||
|
name = status_names[status]
|
||||||
|
status_button = Button(text=name, size_hint_y=None, height=dp(50))
|
||||||
|
status_button.status = status
|
||||||
|
status_button.bind(on_release=set_value)
|
||||||
|
self.dropdown.add_widget(status_button)
|
||||||
|
|
||||||
|
self.dropdown.bind(on_select=select)
|
||||||
|
|
||||||
def set_height(self, instance, value):
|
def set_height(self, instance, value):
|
||||||
self.height = max([child.texture_size[1] for child in self.children])
|
self.height = max([child.texture_size[1] for child in self.children])
|
||||||
|
|
||||||
|
@ -328,7 +351,8 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||||
self.finding_text = data["finding"]["text"]
|
self.finding_text = data["finding"]["text"]
|
||||||
self.location_text = data["location"]["text"]
|
self.location_text = data["location"]["text"]
|
||||||
self.entrance_text = data["entrance"]["text"]
|
self.entrance_text = data["entrance"]["text"]
|
||||||
self.found_text = data["found"]["text"]
|
self.status_text = data["status"]["text"]
|
||||||
|
self.hint = data["status"]["hint"]
|
||||||
self.height = self.minimum_height
|
self.height = self.minimum_height
|
||||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||||
|
|
||||||
|
@ -338,13 +362,21 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||||
return True
|
return True
|
||||||
if self.index: # skip header
|
if self.index: # skip header
|
||||||
if self.collide_point(*touch.pos):
|
if self.collide_point(*touch.pos):
|
||||||
if self.selected:
|
status_label = self.ids["status"]
|
||||||
|
if status_label.collide_point(*touch.pos):
|
||||||
|
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||||
|
return
|
||||||
|
ctx = App.get_running_app().ctx
|
||||||
|
if ctx.slot == self.hint["receiving_player"]: # If this player owns this hint
|
||||||
|
# open a dropdown
|
||||||
|
self.dropdown.open(self.ids["status"])
|
||||||
|
elif self.selected:
|
||||||
self.parent.clear_selection()
|
self.parent.clear_selection()
|
||||||
else:
|
else:
|
||||||
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
||||||
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
||||||
if self.entrance_text != "Vanilla"
|
if self.entrance_text != "Vanilla"
|
||||||
else "", ". (", self.found_text.lower(), ")"))
|
else "", ". (", self.status_text.lower(), ")"))
|
||||||
temp = MarkupLabel(text).markup
|
temp = MarkupLabel(text).markup
|
||||||
text = "".join(
|
text = "".join(
|
||||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||||
|
@ -358,18 +390,16 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||||
for child in self.children:
|
for child in self.children:
|
||||||
if child.collide_point(*touch.pos):
|
if child.collide_point(*touch.pos):
|
||||||
key = child.sort_key
|
key = child.sort_key
|
||||||
parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
if key == "status":
|
||||||
|
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
|
||||||
|
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||||
if key == parent.sort_key:
|
if key == parent.sort_key:
|
||||||
# second click reverses order
|
# second click reverses order
|
||||||
parent.reversed = not parent.reversed
|
parent.reversed = not parent.reversed
|
||||||
else:
|
else:
|
||||||
parent.sort_key = key
|
parent.sort_key = key
|
||||||
parent.reversed = False
|
parent.reversed = False
|
||||||
break
|
App.get_running_app().update_hints()
|
||||||
else:
|
|
||||||
logging.warning("Did not find clicked header for sorting.")
|
|
||||||
|
|
||||||
App.get_running_app().update_hints()
|
|
||||||
|
|
||||||
def apply_selection(self, rv, index, is_selected):
|
def apply_selection(self, rv, index, is_selected):
|
||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
|
@ -663,7 +693,7 @@ class GameManager(App):
|
||||||
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
||||||
|
|
||||||
def update_hints(self):
|
def update_hints(self):
|
||||||
hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
|
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||||
self.log_panels["Hints"].refresh_hints(hints)
|
self.log_panels["Hints"].refresh_hints(hints)
|
||||||
|
|
||||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||||
|
@ -719,6 +749,22 @@ class UILog(RecycleView):
|
||||||
element.height = element.texture_size[1]
|
element.height = element.texture_size[1]
|
||||||
|
|
||||||
|
|
||||||
|
status_names: typing.Dict[HintStatus, str] = {
|
||||||
|
HintStatus.HINT_FOUND: "Found",
|
||||||
|
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "No Priority",
|
||||||
|
HintStatus.HINT_AVOID: "Avoid",
|
||||||
|
HintStatus.HINT_PRIORITY: "Priority",
|
||||||
|
}
|
||||||
|
status_colors: typing.Dict[HintStatus, str] = {
|
||||||
|
HintStatus.HINT_FOUND: "green",
|
||||||
|
HintStatus.HINT_UNSPECIFIED: "white",
|
||||||
|
HintStatus.HINT_NO_PRIORITY: "cyan",
|
||||||
|
HintStatus.HINT_AVOID: "salmon",
|
||||||
|
HintStatus.HINT_PRIORITY: "plum",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HintLog(RecycleView):
|
class HintLog(RecycleView):
|
||||||
header = {
|
header = {
|
||||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||||
|
@ -726,12 +772,13 @@ class HintLog(RecycleView):
|
||||||
"finding": {"text": "[u]Finding Player[/u]"},
|
"finding": {"text": "[u]Finding Player[/u]"},
|
||||||
"location": {"text": "[u]Location[/u]"},
|
"location": {"text": "[u]Location[/u]"},
|
||||||
"entrance": {"text": "[u]Entrance[/u]"},
|
"entrance": {"text": "[u]Entrance[/u]"},
|
||||||
"found": {"text": "[u]Status[/u]"},
|
"status": {"text": "[u]Status[/u]",
|
||||||
|
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||||
"striped": True,
|
"striped": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
sort_key: str = ""
|
sort_key: str = ""
|
||||||
reversed: bool = False
|
reversed: bool = True
|
||||||
|
|
||||||
def __init__(self, parser):
|
def __init__(self, parser):
|
||||||
super(HintLog, self).__init__()
|
super(HintLog, self).__init__()
|
||||||
|
@ -739,8 +786,18 @@ class HintLog(RecycleView):
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
def refresh_hints(self, hints):
|
def refresh_hints(self, hints):
|
||||||
|
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||||
|
self.scroll_y = 1.0
|
||||||
data = []
|
data = []
|
||||||
|
ctx = App.get_running_app().ctx
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
|
if not hint.get("status"): # Allows connecting to old servers
|
||||||
|
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
||||||
|
hint_status_node = self.parser.handle_node({"type": "color",
|
||||||
|
"color": status_colors.get(hint["status"], "red"),
|
||||||
|
"text": status_names.get(hint["status"], "Unknown")})
|
||||||
|
if hint["status"] != HintStatus.HINT_FOUND and hint["receiving_player"] == ctx.slot:
|
||||||
|
hint_status_node = f"[u]{hint_status_node}[/u]"
|
||||||
data.append({
|
data.append({
|
||||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||||
"item": {"text": self.parser.handle_node({
|
"item": {"text": self.parser.handle_node({
|
||||||
|
@ -758,9 +815,10 @@ class HintLog(RecycleView):
|
||||||
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
||||||
"color": "blue", "text": hint["entrance"]
|
"color": "blue", "text": hint["entrance"]
|
||||||
if hint["entrance"] else "Vanilla"})},
|
if hint["entrance"] else "Vanilla"})},
|
||||||
"found": {
|
"status": {
|
||||||
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
|
"text": hint_status_node,
|
||||||
"text": "Found" if hint["found"] else "Not Found"})},
|
"hint": hint,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
||||||
|
@ -771,7 +829,7 @@ class HintLog(RecycleView):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hint_sorter(element: dict) -> str:
|
def hint_sorter(element: dict) -> str:
|
||||||
return ""
|
return element["status"]["hint"]["status"] # By status by default
|
||||||
|
|
||||||
def fix_heights(self):
|
def fix_heights(self):
|
||||||
"""Workaround fix for divergent texture and layout heights"""
|
"""Workaround fix for divergent texture and layout heights"""
|
||||||
|
|
Loading…
Reference in New Issue