Skip to content

Commit

Permalink
Introduce 'Hint Priority' concept
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilyV99 committed Jun 10, 2024
1 parent 84a6d50 commit b5c8a4e
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 40 deletions.
9 changes: 8 additions & 1 deletion CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,14 @@ async def shutdown(self):
await self.ui_task
if self.input_task:
self.input_task.cancel()


# Hints
def update_hint(self, location: int, finding_player: int, priority: typing.Optional[bool]):
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
if priority is not None:
msg["priority"] = priority
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],
Expand Down
2 changes: 1 addition & 1 deletion Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def write_multidata():
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)
Expand Down
72 changes: 51 additions & 21 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,16 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b
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[NetUtils.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: NetUtils.Hint, new_hint: NetUtils.Hint) -> None:
self.hints[team, slot] = [new_hint if hint == old_hint else hint for hint in self.hints[team, slot]]

# "events"

def on_goal_achieved(self, client: Client):
Expand Down Expand Up @@ -1035,7 +1045,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
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], prioritize: bool) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
Expand All @@ -1048,27 +1058,26 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
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))
item_flags, prioritize))

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, prioritize: bool) -> typing.List[NetUtils.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, prioritize)


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, prioritize: bool) -> typing.List[NetUtils.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)]
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, prioritize)]
return []


def format_hint(ctx: Context, team: int, hint: NetUtils.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 " \
Expand All @@ -1077,7 +1086,7 @@ 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 + (". (found)" if hint.found else ("." if hint.prioritize else ". (non-priority)"))


def json_format_send_event(net_item: NetworkItem, receiving_player: int):
Expand Down Expand Up @@ -1500,9 +1509,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
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, True)
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, True)

else:
game = self.ctx.games[self.client.slot]
Expand All @@ -1522,16 +1531,16 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
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, True))
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, True)
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, False))
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, False)

else:
self.output(response)
Expand Down Expand Up @@ -1803,13 +1812,34 @@ 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, False))
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"]
priority = args["priority"]
if type(player) is not int or type(location) is not int or (priority is not None and type(priority) != bool):
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint',
"original_cmd": cmd}])
return
hint = ctx.get_hint(client.team, player, location)
new_hint = hint
if not hint:
return # Ignored safely
needs_update: bool = False
if priority is not None:
new_hint = new_hint.re_prioritize(ctx, priority)
if hint != new_hint:
ctx.replace_hint(client.team, client.slot, hint, new_hint)
ctx.save()
ctx.on_changed_hints(client.team, player)

elif cmd == 'StatusUpdate':
update_client_status(ctx, client, args["status"])

Expand Down Expand Up @@ -2110,9 +2140,9 @@ def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
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, True))
else: # item name or id
hints = collect_hints(self.ctx, team, slot, item)
hints = collect_hints(self.ctx, team, slot, item, True)

if hints:
self.ctx.notify_hints(team, hints)
Expand Down Expand Up @@ -2146,14 +2176,14 @@ def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:

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, False)
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, False))
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
hints = collect_hint_location_name(self.ctx, team, slot, location, False)
if hints:
self.ctx.notify_hints(team, hints)
else:
Expand Down
13 changes: 11 additions & 2 deletions NetUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,21 @@ class Hint(typing.NamedTuple):
found: bool
entrance: str = ""
item_flags: int = 0
prioritized: bool = True

def re_check(self, ctx, team) -> Hint:
if self.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)
self.item_flags, self.prioritized)
return self

def re_prioritize(self, ctx, priority: bool) -> Hint:
if priority != self.prioritized:
return Hint(self.receiving_player, self.finding_player, self.location, self.item, self.found, self.entrance,
self.item_flags, priority)
return self

def __hash__(self):
Expand All @@ -334,8 +341,10 @@ def as_network_message(self) -> dict:
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, "(found)", type="color", color="green")
elif self.prioritized:
add_json_text(parts, "(priority)", type="color", color="red")
else:
add_json_text(parts, "(not found)", type="color", color="red")
add_json_text(parts, "(non-priority)", type="color", color="red")

return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
Expand Down
11 changes: 11 additions & 0 deletions docs/network protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,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)
Expand Down Expand Up @@ -341,6 +342,16 @@ 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. |
| priority | bool | Optional. If included, sets the priority of the hint to this status. |

### 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)

Expand Down
47 changes: 32 additions & 15 deletions kvui.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def __init__(self):
self.location_text = ""
self.entrance_text = ""
self.found_text = ""
self.meta = {}
for child in self.children:
child.bind(texture_size=self.set_height)

Expand All @@ -326,28 +327,39 @@ def refresh_view_attrs(self, rv, index, data):
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.found_text = data["status"]["text"]
self.meta = data["status"]["meta"]
self.height = self.minimum_height
return super(HintLabel, self).refresh_view_attrs(rv, index, data)

def on_touch_down(self, touch):
""" Add selection on touch down """
if super(HintLabel, self).on_touch_down(touch):
return True
alt: bool = touch.button == "right" or touch.is_double_tap
if self.index: # skip header
if self.collide_point(*touch.pos):
if self.selected:
self.parent.clear_selection()
if alt:
ctx = App.get_running_app().ctx
if ctx.slot == int(self.meta["receiving_player"]): # If this player owns this hint
ctx.update_hint(int(self.meta["location"]),
int(self.meta["finding_player"]),
not bool(self.meta["prioritized"]))
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(), ")"))
temp = MarkupLabel(text).markup
text = "".join(
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
Clipboard.copy(escape_markup(text).replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch)
if 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(), ")"))
temp = MarkupLabel(text).markup
text = "".join(
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
Clipboard.copy(
escape_markup(text).replace("&amp;", "&").replace("&bl;", "[").replace("&br;", "]"))
return self.parent.select_with_touch(self.index, touch)
else:
parent = self.parent
parent.clear_selection()
Expand Down Expand Up @@ -716,7 +728,8 @@ 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]",
"meta": {"receiving_player": -1, "location": -1, "finding_player": -1, "prioritized": False}},
"striped": True,
}

Expand Down Expand Up @@ -748,9 +761,13 @@ def refresh_hints(self, hints):
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
"color": "blue", "text": hint["entrance"]
if hint["entrance"] else "Vanilla"})},
"found": {
"status": {
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
"text": "Found" if hint["found"] else "Not Found"})},
"text": "Found" if hint["found"] else (
"Priority" if hint["prioritized"] else "Non-Priority")}),
"meta": {"receiving_player": hint["receiving_player"], "location": hint["location"],
"finding_player": hint["finding_player"], "prioritized": hint["prioritized"]},
},
})

data.sort(key=self.hint_sorter, reverse=self.reversed)
Expand Down

0 comments on commit b5c8a4e

Please sign in to comment.