-
Notifications
You must be signed in to change notification settings - Fork 701
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
Jouramie
wants to merge
32
commits into
ArchipelagoMW:main
Choose a base branch
from
agilbert1412:StardewValley/text-client-explain-command
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+381
−26
Open
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 d2eee35
add basic text client with UT integration
Jouramie 14f3c61
self review
Jouramie 77e818b
disable menu entry if tracker feature flag if False (which it is)
Jouramie 79c0ba5
except ImportError
Jouramie 6168034
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie 0d11c51
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie 35479f4
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie 2666698
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie 1c4efc8
Merge branch 'main' into StardewValley/stardew-text-client
Jouramie feab978
Merge remote-tracking branch 'origin/main' into StardewValley/stardew…
Jouramie b230b11
Merge remote-tracking branch 'archipelago/main' into StardewValley/st…
Jouramie 21bd402
prove that's it's possible to log explain
Jouramie 9f2011f
add missing command
Jouramie c86dc7b
implement commands
Jouramie e24c338
add command to explain more
Jouramie f2204a8
allow no expectation, and add more space in client mode
Jouramie ca894ab
copied stuff from UT to get a working state
Jouramie 485d254
use ut context as base tracker
Jouramie 45d3d04
add fuzzy matching on names
Jouramie f67c31a
best effet to find out if ut is installed
Jouramie bd98460
best effet to find out if ut is installed
Jouramie bf443eb
move icon in stardew apworld
Jouramie 71f3f5f
adapt to new ut versions
Jouramie 2a50bd4
color true and false for better readability
Jouramie b53c982
add color to location, region, entrances and items
Jouramie 72a2685
fix count explain being way to big
Jouramie 4738ce7
add colors to other commands
Jouramie 9260891
add color in count explanation
Jouramie 2227fc7
Pushback location explains in more explain
Jouramie 770dae0
properly color count subrules
Jouramie f1d7f7b
change get_updated_state to call directly updateTracker
Jouramie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|
||
|
||
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 not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).stateWe did this just in case the return needs to expand again, so it's less likely to break things in the future
There was a problem hiding this comment.
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