-
Notifications
You must be signed in to change notification settings - Fork 774
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
base: main
Are you sure you want to change the base?
Changes from 33 commits
7eae4f7
d2eee35
14f3c61
77e818b
79c0ba5
6168034
0d11c51
35479f4
2666698
1c4efc8
feab978
b230b11
21bd402
9f2011f
c86dc7b
e24c338
f2204a8
ca894ab
485d254
45d3d04
f67c31a
bd98460
bf443eb
71f3f5f
2a50bd4
b53c982
72a2685
4738ce7
9260891
2227fc7
770dae0
f1d7f7b
90f920a
7af0458
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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).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)) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I heard something in dev channels recently about not being allowed to use copyrighted icons in the launcher, but some games are doing it. Can we get a confirmation of is this fair use or not? If needed, I can provide written permission from ConcernedApe's company to use game assets in non-stardew parts of the randomizer |
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.
Any chance this regex could get maybe a comment or some splitting down? Regex are famously unreadable after about 7 seconds after merging