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:
Emily 2024-11-28 20:10:31 -05:00 committed by GitHub
parent b972e8c071
commit b783eab1e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 288 additions and 72 deletions

View File

@ -23,7 +23,7 @@ if __name__ == "__main__":
from MultiServer import CommandProcessor
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 worlds import network_data_package, AutoWorldRegister
import os
@ -412,6 +412,7 @@ class CommonContext:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
self.ui.update_hints()
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
@ -551,7 +552,14 @@ class CommonContext:
await self.ui_task
if self.input_task:
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
async def prepare_data_package(self, relevant_games: typing.Set[str],
remote_date_package_versions: typing.Dict[str, int],

View File

@ -276,7 +276,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(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)
if location.item.player not in multiworld.groups:
precollected_hints[location.item.player].add(hint)

View File

@ -41,7 +41,8 @@ import NetUtils
import Utils
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
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)
colorama.init()
@ -228,7 +229,7 @@ class Context:
self.hint_cost = hint_cost
self.location_check_points = location_check_points
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.remaining_mode: str = remaining_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 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:
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
self.hints[hint_team, hint_slot] = {
hint.re_check(self, hint_team) for hint in
self.hints[hint_team, hint_slot]
}
if team != hint_team and team is not None:
continue # Check specified team only, all if team is None
if slot != hint_slot and slot is not None:
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):
self.recheck_hints(team, slot)
@ -711,7 +728,7 @@ class Context:
else:
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):
"""Send and remember hints."""
if only_new:
@ -749,6 +766,17 @@ class Context:
for client in clients:
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"
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),
"checked_locations": new_locations, # send back new checks only
}])
old_hints = ctx.hints[team, slot].copy()
ctx.recheck_hints(team, slot)
if old_hints != ctx.hints[team, slot]:
ctx.on_changed_hints(team, slot)
updated_slots: typing.Set[tuple[int, int]] = set()
ctx.recheck_hints(team, slot, updated_slots)
for hint_team, hint_slot in updated_slots:
ctx.on_changed_hints(hint_team, hint_slot)
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 = []
slots: typing.Set[int] = {slot}
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]
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags))
prev_hint = ctx.get_hint(team, slot, location_id)
if prev_hint:
hints.append(prev_hint)
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
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]
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))
if any(result):
item_id, receiving_player, item_flags = result
found = seeked_location in ctx.location_checks[team, slot]
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 []
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 " \
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]} " \
@ -1099,7 +1155,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
if 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):
@ -1503,7 +1560,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
points_available = get_client_points(self.ctx, self.client)
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:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
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.")
hints = []
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:
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:
game = self.ctx.games[self.client.slot]
@ -1551,16 +1608,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = []
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
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
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
hints = []
for loc_name in self.ctx.location_name_groups[game][hint_name]:
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
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:
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]
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))
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
if locs and create_as_hint:
ctx.save()
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':
update_client_status(ctx, client, args["status"])
@ -2143,9 +2238,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
hints = []
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
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
hints = collect_hints(self.ctx, team, slot, item)
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
if hints:
self.ctx.notify_hints(team, hints)
@ -2179,14 +2274,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
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]:
hints = []
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):
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:
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:
self.ctx.notify_hints(team, hints)
else:

View File

@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum):
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):
spectator = 0b00
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})
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):
receiving_player: int
finding_player: int
@ -305,14 +327,21 @@ class Hint(typing.NamedTuple):
found: bool
entrance: str = ""
item_flags: int = 0
status: HintStatus = HintStatus.HINT_UNSPECIFIED
def re_check(self, ctx, team) -> Hint:
if self.found:
if self.found and self.status == HintStatus.HINT_FOUND:
return self
found = self.location in ctx.location_checks[team, self.finding_player]
if found:
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
self.item_flags)
return self._replace(found=found, status=HintStatus.HINT_FOUND)
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
def __hash__(self):
@ -334,10 +363,8 @@ class Hint(typing.NamedTuple):
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, "(not found)", type="color", color="red")
add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color",
color=status_colors.get(self.status, "red"))
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,

View File

@ -421,7 +421,8 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# 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)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name == "PlandoItem":

View File

@ -59,7 +59,7 @@
finding_text: "Finding Player"
location_text: "Location"
entrance_text: "Entrance"
found_text: "Found?"
status_text: "Status"
TooltipLabel:
id: receiving
sort_key: 'receiving'
@ -96,9 +96,9 @@
valign: 'center'
pos_hint: {"center_y": 0.5}
TooltipLabel:
id: found
sort_key: 'found'
text: root.found_text
id: status
sort_key: 'status'
text: root.status_text
halign: 'center'
valign: 'center'
pos_hint: {"center_y": 0.5}

View File

@ -272,6 +272,7 @@ These packets are sent purely from client to server. They are not accepted by cl
* [Sync](#Sync)
* [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts)
* [UpdateHint](#UpdateHint)
* [StatusUpdate](#StatusUpdate)
* [Say](#Say)
* [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. |
| 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
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
View File

@ -52,6 +52,7 @@ from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.dropdown import DropDown
from kivy.utils import escape_markup
from kivy.lang import Builder
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)
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
from Utils import async_start, get_input_text_from_response
if typing.TYPE_CHECKING:
@ -300,11 +301,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
index = None
dropdown: DropDown
def __init__(self):
super(HintLabel, self).__init__()
@ -313,10 +314,32 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
self.finding_text = ""
self.location_text = ""
self.entrance_text = ""
self.found_text = ""
self.status_text = ""
self.hint = {}
for child in self.children:
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):
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.location_text = data["location"]["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
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
@ -338,13 +362,21 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
return True
if self.index: # skip header
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()
else:
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
self.finding_text, "\'s World", (" at " + self.entrance_text)
if self.entrance_text != "Vanilla"
else "", ". (", self.found_text.lower(), ")"))
else "", ". (", self.status_text.lower(), ")"))
temp = MarkupLabel(text).markup
text = "".join(
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:
if child.collide_point(*touch.pos):
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:
# second click reverses order
parent.reversed = not parent.reversed
else:
parent.sort_key = key
parent.reversed = False
break
else:
logging.warning("Did not find clicked header for sorting.")
App.get_running_app().update_hints()
App.get_running_app().update_hints()
def apply_selection(self, rv, index, is_selected):
""" 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"
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)
# 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]
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):
header = {
"receiving": {"text": "[u]Receiving Player[/u]"},
@ -726,12 +772,13 @@ class HintLog(RecycleView):
"finding": {"text": "[u]Finding Player[/u]"},
"location": {"text": "[u]Location[/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,
}
sort_key: str = ""
reversed: bool = False
reversed: bool = True
def __init__(self, parser):
super(HintLog, self).__init__()
@ -739,8 +786,18 @@ class HintLog(RecycleView):
self.parser = parser
def refresh_hints(self, hints):
if not hints: # Fix the scrolling looking visually wrong in some edge cases
self.scroll_y = 1.0
data = []
ctx = App.get_running_app().ctx
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({
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
"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",
"color": "blue", "text": hint["entrance"]
if hint["entrance"] else "Vanilla"})},
"found": {
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
"text": "Found" if hint["found"] else "Not Found"})},
"status": {
"text": hint_status_node,
"hint": hint,
},
})
data.sort(key=self.hint_sorter, reverse=self.reversed)
@ -771,7 +829,7 @@ class HintLog(RecycleView):
@staticmethod
def hint_sorter(element: dict) -> str:
return ""
return element["status"]["hint"]["status"] # By status by default
def fix_heights(self):
"""Workaround fix for divergent texture and layout heights"""