Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stardew Valley: Add tracker text client integrated with UT and commands to explain the logic #4378

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7eae4f7
add basic text client with UT integration
Jouramie Jun 28, 2024
d2eee35
add basic text client with UT integration
Jouramie Jun 28, 2024
14f3c61
self review
Jouramie Jun 28, 2024
77e818b
disable menu entry if tracker feature flag if False (which it is)
Jouramie Jun 28, 2024
79c0ba5
except ImportError
Jouramie Jul 3, 2024
6168034
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie Jul 3, 2024
0d11c51
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie Jul 4, 2024
35479f4
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie Jul 7, 2024
2666698
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie Jul 14, 2024
1c4efc8
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie Oct 15, 2024
feab978
Merge remote-tracking branch 'origin/main' into StardewValley/stardew…
Jouramie Nov 30, 2024
b230b11
Merge remote-tracking branch 'archipelago/main' into StardewValley/st…
Jouramie Dec 9, 2024
21bd402
prove that's it's possible to log explain
Jouramie Jun 30, 2024
9f2011f
add missing command
Jouramie Jul 1, 2024
c86dc7b
implement commands
Jouramie Jul 1, 2024
e24c338
add command to explain more
Jouramie Dec 15, 2024
f2204a8
allow no expectation, and add more space in client mode
Jouramie Dec 15, 2024
ca894ab
copied stuff from UT to get a working state
Jouramie Dec 15, 2024
485d254
use ut context as base tracker
Jouramie Dec 15, 2024
45d3d04
add fuzzy matching on names
Jouramie Dec 15, 2024
f67c31a
best effet to find out if ut is installed
Jouramie Dec 15, 2024
bd98460
best effet to find out if ut is installed
Jouramie Dec 15, 2024
bf443eb
move icon in stardew apworld
Jouramie Dec 15, 2024
71f3f5f
adapt to new ut versions
Jouramie Dec 15, 2024
2a50bd4
color true and false for better readability
Jouramie Dec 15, 2024
b53c982
add color to location, region, entrances and items
Jouramie Dec 15, 2024
72a2685
fix count explain being way to big
Jouramie Dec 15, 2024
4738ce7
add colors to other commands
Jouramie Dec 15, 2024
9260891
add color in count explanation
Jouramie Dec 15, 2024
2227fc7
Pushback location explains in more explain
Jouramie Dec 15, 2024
770dae0
properly color count subrules
Jouramie Dec 15, 2024
f1d7f7b
change get_updated_state to call directly updateTracker
Jouramie Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions worlds/stardew_valley/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import launch_subprocess, components, Component, icon_paths, Type
from . import rules
from .bundles.bundle_room import BundleRoom
from .bundles.bundles import get_all_bundles
Expand Down Expand Up @@ -35,6 +36,7 @@
UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed"

client_version = 0
TRACKER_ENABLED = True


class StardewLocation(Location):
Expand Down Expand Up @@ -62,6 +64,27 @@ class StardewWebWorld(WebWorld):
)]


if TRACKER_ENABLED:
from .. import user_folder
import os

# Best effort to detect if universal tracker is installed
if any("tracker.apworld" in f.name for f in os.scandir(user_folder)):
def launch_client():
from .client import launch
launch_subprocess(launch, name="Stardew Valley Tracker")


components.append(Component(
"Stardew Valley Tracker",
func=launch_client,
component_type=Type.CLIENT,
icon='stardew'
))

icon_paths['stardew'] = f"ap:{__name__}/stardew.png"


class StardewValleyWorld(World):
"""
Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests,
Expand Down
250 changes: 250 additions & 0 deletions worlds/stardew_valley/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
from __future__ import annotations

import asyncio
import re
# webserver imports
import urllib.parse

import Utils
from BaseClasses import MultiWorld, CollectionState
from CommonClient import logger, get_base_parser, gui_enabled, server_loop
from MultiServer import mark_raw
from NetUtils import JSONMessagePart
from .logic.logic import StardewLogic
from .stardew_rule.rule_explain import explain, ExplainMode, RuleExplanation

try:
from worlds.tracker.TrackerClient import TrackerGameContext, TrackerCommandProcessor as ClientCommandProcessor, UT_VERSION, updateTracker # noqa

tracker_loaded = True
except ImportError:
from CommonClient import CommonContext, ClientCommandProcessor


class TrackerGameContextMixin:
"""Expecting the TrackerGameContext to have these methods."""
multiworld: MultiWorld
player_id: int

def build_gui(self, manager):
...

def run_generator(self):
...

def load_kv(self):
...


class TrackerGameContext(CommonContext, TrackerGameContextMixin):
pass


tracker_loaded = False
UT_VERSION = "Not found"


class StardewCommandProcessor(ClientCommandProcessor):
ctx: StardewClientContext

@mark_raw
def _cmd_explain(self, location: str = ""):
"""Explain the logic behind a location."""
if self.ctx.logic is None:
logger.warning("Internal logic was not able to load, check your yamls and relaunch.")
return

try:
rule = self.ctx.logic.region.can_reach_location(location)
expl = explain(rule, get_updated_state(self.ctx), expected=None, mode=ExplainMode.CLIENT)
except KeyError:

result, usable, response = Utils.get_intended_text(location, [loc.name for loc in self.ctx.multiworld.get_locations(1)])
if usable:
rule = self.ctx.logic.region.can_reach_location(result)
expl = explain(rule, get_updated_state(self.ctx), expected=None, mode=ExplainMode.CLIENT)
else:
logger.warning(response)
return

self.ctx.previous_explanation = expl
self.ctx.ui.print_json(parse_explanation(expl))

@mark_raw
def _cmd_explain_item(self, item: str = ""):
"""Explain the logic behind a game item."""
if self.ctx.logic is None:
logger.warning("Internal logic was not able to load, check your yamls and relaunch.")
return

result, usable, response = Utils.get_intended_text(item, self.ctx.logic.registry.item_rules.keys())
if usable:
rule = self.ctx.logic.has(result)
expl = explain(rule, get_updated_state(self.ctx), expected=None, mode=ExplainMode.CLIENT)
else:
logger.warning(response)
return

self.ctx.previous_explanation = expl
self.ctx.ui.print_json(parse_explanation(expl))

@mark_raw
def _cmd_explain_missing(self, location: str = ""):
"""Explain the logic behind a location, while skipping the rules that are already satisfied."""
if self.ctx.logic is None:
logger.warning("Internal logic was not able to load, check your yamls and relaunch.")
return

try:
rule = self.ctx.logic.region.can_reach_location(location)
state = get_updated_state(self.ctx)
simplified, _ = rule.evaluate_while_simplifying(state)
expl = explain(simplified, state, mode=ExplainMode.CLIENT)
except KeyError:

result, usable, response = Utils.get_intended_text(location, [loc.name for loc in self.ctx.multiworld.get_locations(1)])
if usable:
rule = self.ctx.logic.region.can_reach_location(result)
state = get_updated_state(self.ctx)
simplified, _ = rule.evaluate_while_simplifying(state)
expl = explain(simplified, state, mode=ExplainMode.CLIENT)
else:
logger.warning(response)
return

self.ctx.previous_explanation = expl
self.ctx.ui.print_json(parse_explanation(expl))

@mark_raw
def _cmd_more(self, index: str = ""):
"""Will tell you what's missing to consider a location in logic."""
if self.ctx.previous_explanation is None:
logger.warning("No previous explanation found.")
return

try:
expl = self.ctx.previous_explanation.more(int(index))
except (ValueError, IndexError):
logger.info("Which previous rule do you want to explained?")
for i, rule in enumerate(self.ctx.previous_explanation.more_explanations):
logger.info(f"/more {i} -> {str(rule)})")
return

self.ctx.previous_explanation = expl
self.ctx.ui.print_json(parse_explanation(expl))

if not tracker_loaded:
del _cmd_explain
del _cmd_explain_missing


class StardewClientContext(TrackerGameContext):
game = "Stardew Valley"
command_processor = StardewCommandProcessor
logic: StardewLogic | None = None
previous_explanation: RuleExplanation | None = None

def make_gui(self):
ui = super().make_gui() # before the kivy imports so kvui gets loaded first

class StardewManager(ui):
base_title = f"Stardew Valley Tracker with UT {UT_VERSION} for AP version" # core appends ap version so this works
ctx: StardewClientContext

def build(self):
container = super().build()
if not tracker_loaded:
logger.info("To enable the tracker page, install Universal Tracker.")

return container

return StardewManager

def setup_logic(self):
if self.multiworld is not None:
self.logic = self.multiworld.worlds[1].logic


def parse_explanation(explanation: RuleExplanation) -> list[JSONMessagePart]:
result_regex = r"(\(|\)| & | -> | \| |\d+x | \[|\](?: ->)?\s*| \(\w+\)|\n\s*)"
splits = re.split(result_regex, str(explanation).strip())

messages = []
for s in splits:
if len(s) == 0:
continue

if s == "True":
messages.append({"type": "color", "color": "green", "text": s})
elif s == "False":
messages.append({"type": "color", "color": "salmon", "text": s})
elif s.startswith("Reach Location "):
messages.append({"type": "text", "text": "Reach Location "})
messages.append({"type": "location_name", "text": s[15:]})
elif s.startswith("Reach Entrance "):
messages.append({"type": "text", "text": "Reach Entrance "})
messages.append({"type": "entrance_name", "text": s[15:]})
elif s.startswith("Reach Region "):
messages.append({"type": "text", "text": "Reach Region "})
messages.append({"type": "color", "color": "yellow", "text": s[13:]})
elif s.startswith("Received event "):
messages.append({"type": "text", "text": "Received event "})
messages.append({"type": "item_name", "text": s[15:]})
elif s.startswith("Received "):
messages.append({"type": "text", "text": "Received "})
messages.append({"type": "item_name", "flags": 0b001, "text": s[9:]})
elif s.startswith("Has "):
if s[4].isdigit():
messages.append({"type": "text", "text": "Has "})
digit_end = re.search(r"\D", s[4:])
digit = s[4:4 + digit_end.start()]
messages.append({"type": "color", "color": "cyan", "text": digit})
messages.append({"type": "text", "text": s[4 + digit_end.start():]})

else:
messages.append({"text": s, "type": "text"})
else:
messages.append({"text": s, "type": "text"})

return messages


def get_updated_state(ctx: TrackerGameContext) -> CollectionState:
return updateTracker(ctx)[3]
Copy link
Contributor

@FarisTheAncient FarisTheAncient Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will work, but updateTracker now returns a CurrentTrackerState named tuple (defined in worlds.tracker if you want to grab that but i don't think you need to) so you should be able to change this to updateTracker(ctx).state

We did this just in case the return needs to expand again, so it's less likely to break things in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll change it to use .state



async def main(args):
ctx = StardewClientContext(args.connect, args.password)

ctx.auth = args.name
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")

if tracker_loaded:
ctx.run_generator()
ctx.setup_logic()
else:
logger.warning("Could not find Universal Tracker.")

if gui_enabled:
ctx.run_gui()
ctx.run_cli()

await ctx.exit_event.wait()
await ctx.shutdown()


def launch(*args):
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args)

if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)

asyncio.run(main(args))
Binary file added worlds/stardew_valley/stardew.ico
Binary file not shown.
Binary file added worlds/stardew_valley/stardew.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion worlds/stardew_valley/stardew_rule/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,17 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul
def rules_count(self):
return len(self.rules)

def __str__(self):
if all(value == 1 for value in self.counter.values()):
return f"Has {self.count} of [{', '.join(str(rule) for rule in self.counter.keys())}]"

return f"Has {self.count} of [{', '.join(f'{value}x {str(rule)}' for rule, value in self.counter.items())}]"

def __repr__(self):
return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]"
if all(value == 1 for value in self.counter.values()):
return f"Has {self.count} of [{', '.join(repr(rule) for rule in self.counter.keys())}]"

return f"Has {self.count} of [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]"


@dataclass(frozen=True)
Expand Down
Loading
Loading