Skip to content

Commit 899d11a

Browse files
committed
Update: Fixed timing and Bot Mode
Shiny Quota - Fixed timing of PC/party check Prof Oak Mode - Changed default wrapped bot mode to Spin. Should be configurable to any built in bot mode via the script
1 parent 8415100 commit 899d11a

3 files changed

Lines changed: 249 additions & 45 deletions

File tree

VERSION.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"package_version": "sq-0.2.0-alpha.1_po-0.2.0-alpha.1",
2+
"package_version": "sq-0.2.2-alpha.0_po-0.5.0-alpha.0",
33
"plugins": {
4-
"prof_oak_mode.py": "0.2.0-alpha.1",
5-
"shiny_quota.py": "0.2.0-alpha.1"
4+
"prof_oak_mode.py": "0.5.0-alpha.0",
5+
"shiny_quota.py": "0.2.2-alpha.0"
66
},
7-
"timestamp": "2025-09-04T17:35:58Z"
7+
"timestamp": "2025-09-04T18:35:18Z"
88
}

plugins/prof_oak_mode.py

Lines changed: 207 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,50 @@
11
# -*- coding: utf-8 -*-
22
# plugins/prof_oak_mode.py
33
#
4-
# Adds a new bot mode "Prof Oak" that wraps the existing Level Grind mode.
5-
# It finds Level Grind dynamically from the bot's mode registry, so it works
6-
# across forks/paths. ShinyQuota keeps handling quota/pausing.
4+
# Prof Oak mode — wraps a configurable base bot mode (default Level Grind).
5+
# Configure inside this file, optionally prompt once, and persist to plugins/ProfOak/config.json.
6+
#
7+
# Quick config:
8+
# PLUGIN_DEFAULT_BASES = ["LevelGrind"] # or ["Spin", "LevelGrind"] etc.
9+
# ASK_ON_FIRST_USE = False # True => one-time console prompt
10+
#
11+
# Optional env override:
12+
# PROFOAK_BASE="Spin" # or "Spin,LevelGrind"
13+
#
14+
# Adding more bases later:
15+
# 1) Put the user-facing name in PLUGIN_DEFAULT_BASES or via env.
16+
# 2) If needed, add an import mapping in _try_import_candidates().
717

8-
from typing import Iterable, Generator, TYPE_CHECKING
18+
from __future__ import annotations
19+
from typing import Iterable, Generator, TYPE_CHECKING, Optional, Sequence, Dict, Any
20+
import json
21+
import os
22+
import sys
23+
from pathlib import Path
924

1025
from modules.plugin_interface import BotPlugin
1126
from modules.context import context
27+
from modules.runtime import get_base_path
1228

1329
if TYPE_CHECKING:
1430
from modules.modes import BotMode # typing only
1531

16-
# -------- logging helpers --------
32+
# ======================== Inline config ========================
33+
# Try these base modes in order if nothing else is set.
34+
# Valid examples: "Level Grind", "Spin"
35+
PLUGIN_DEFAULT_BASES: list[str] = ["Spin"] # e.g., ["Spin", "Level Grind"]
36+
37+
# Ask once on first use (console prompt). The choice is saved to plugins/ProfOak/config.json
38+
ASK_ON_FIRST_USE: bool = False
39+
40+
# ===============================================================
41+
42+
# ---- paths (persist choice here so you don't get asked again) ----
43+
PROFOAK_DIR = get_base_path() / "plugins" / "ProfOak"
44+
PROFOAK_DIR.mkdir(parents=True, exist_ok=True)
45+
CONFIG_PATH = PROFOAK_DIR / "config.json"
46+
47+
# ---------------- Tiny logging helpers ----------------
1748
def _log_info(msg: str) -> None:
1849
for attr in ("logger", "log"):
1950
lg = getattr(context, attr, None)
@@ -39,72 +70,216 @@ def _log_warn(msg: str) -> None:
3970
pass
4071
print(f"WARNING: {msg}")
4172

42-
# -------- resolve Level Grind base class --------
43-
def _find_level_grind_base():
44-
# 1) Known import paths on some forks
73+
# ---------------- Convenience ----------------
74+
def _norm_name(s: str) -> str:
75+
return s.strip().lower().replace(" ", "").replace("_", "-")
76+
77+
def _parse_bases(value: object) -> list[str]:
78+
"""Accept str ('Spin,LevelGrind') or list/tuple; return cleaned list."""
79+
names: list[str] = []
80+
if isinstance(value, str):
81+
parts = [p.strip() for p in value.split(",") if p.strip()]
82+
names = parts or [value.strip()]
83+
elif isinstance(value, (list, tuple)):
84+
for v in value:
85+
if isinstance(v, str) and v.strip():
86+
names.append(v.strip())
87+
# de-dup while preserving order
88+
seen = set(); out = []
89+
for n in names:
90+
k = _norm_name(n)
91+
if k and k not in seen:
92+
seen.add(k); out.append(n)
93+
return out
94+
95+
def _load_saved_choice() -> list[str] | None:
4596
try:
46-
from modules.built_in_modes.level_grind import LevelGrind as _Base # type: ignore
47-
return _Base
97+
if CONFIG_PATH.exists():
98+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
99+
if isinstance(data, dict):
100+
if "base_modes" in data:
101+
return _parse_bases(data["base_modes"])
102+
if "base_mode" in data:
103+
return _parse_bases(data["base_mode"])
48104
except Exception:
49105
pass
106+
return None
107+
108+
def _save_choice(bases: list[str]) -> None:
50109
try:
51-
from modules.modes.level_grind import LevelGrind as _Base # type: ignore
52-
return _Base
110+
payload = {"base_modes": bases}
111+
CONFIG_PATH.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
112+
_log_info(f"[ProfOak] Saved base selection to {CONFIG_PATH.name}: {bases}")
113+
except Exception as e:
114+
_log_warn(f"[ProfOak] Could not save base selection: {e}")
115+
116+
# ---------------- Discovery helpers ----------------
117+
def _try_import_candidates(canon: str):
118+
"""
119+
Try typical import paths for a given canonical name, return class or None.
120+
canon examples: 'levelgrind', 'spin'
121+
"""
122+
name_map = {
123+
"levelgrind": ("level_grind", "LevelGrind"),
124+
"spin": ("spin", "Spin"),
125+
# Add more here later, e.g.:
126+
# "sweetscent": ("sweet_scent", "SweetScent"),
127+
}
128+
mod_name, class_name = name_map.get(canon, (canon, canon.capitalize()))
129+
130+
# 1) built-in modes
131+
try:
132+
m = __import__(f"modules.built_in_modes.{mod_name}", fromlist=[class_name])
133+
return getattr(m, class_name, None)
53134
except Exception:
54135
pass
55136

56-
# 2) Dynamic discovery via the bot's registry
137+
# 2) regular modes
138+
try:
139+
m = __import__(f"modules.modes.{mod_name}", fromlist=[class_name])
140+
return getattr(m, class_name, None)
141+
except Exception:
142+
pass
143+
144+
return None
145+
146+
def _find_mode_in_registry(canon: str):
147+
"""Search the bot's registered modes by normalized name."""
57148
try:
58149
from modules.modes import get_bot_modes # type: ignore
59150
for cls in get_bot_modes():
60-
name_fn = getattr(cls, "name", None)
61-
cls_name = getattr(cls, "__name__", "").lower()
151+
if cls is None:
152+
continue
153+
# Check static .name()
154+
mode_name = None
62155
try:
156+
name_fn = getattr(cls, "name", None)
63157
mode_name = name_fn() if callable(name_fn) else None
64158
except Exception:
65159
mode_name = None
66160

67-
if (
68-
(isinstance(mode_name, str) and mode_name.strip().lower() in {"level grind", "level-grind", "level_grind"})
69-
or cls_name in {"levelgrind", "level_grind", "levelgrindmode"}
70-
):
161+
cand = (
162+
_norm_name(str(mode_name)) if isinstance(mode_name, str) else None
163+
) or _norm_name(getattr(cls, "__name__", ""))
164+
165+
if cand == canon:
166+
return cls
167+
168+
# tolerant variants
169+
if canon == "levelgrind" and cand in {"level_grind", "levelgrindmode"}:
170+
return cls
171+
if canon == "spin" and cand in {"spinner", "spinmode"}:
71172
return cls
72173
except Exception:
73174
pass
74-
75175
return None
76176

77-
# -------- plugin that registers our mode --------
177+
def _discover_available(basenames: list[str]) -> Dict[str, Any]:
178+
"""Return {pretty_name: class} for any of the requested basenames that are available."""
179+
out: Dict[str, Any] = {}
180+
for raw in basenames:
181+
canon = _norm_name(raw)
182+
cls = _try_import_candidates(canon) or _find_mode_in_registry(canon)
183+
if cls:
184+
out[raw] = cls
185+
return out
186+
187+
def _resolve_base_class(preferred: Sequence[str]):
188+
"""Return (BaseClass, chosen_pretty_name)."""
189+
for raw in preferred:
190+
canon = _norm_name(raw)
191+
cls = _try_import_candidates(canon) or _find_mode_in_registry(canon)
192+
if cls:
193+
return cls, raw
194+
return None, None
195+
196+
# ---------------- Selection logic ----------------
197+
def _get_preferred_bases() -> list[str]:
198+
# 1) saved choice
199+
saved = _load_saved_choice()
200+
if saved:
201+
return saved
202+
203+
# 2) env var
204+
env = os.getenv("PROFOAK_BASE")
205+
if env and env.strip():
206+
bases = _parse_bases(env)
207+
if bases:
208+
return bases
209+
210+
# 3) inline defaults
211+
return list(PLUGIN_DEFAULT_BASES)
212+
213+
def _maybe_prompt_once(defaults: list[str]) -> list[str]:
214+
"""If ASK_ON_FIRST_USE and interactive terminal, prompt once and persist."""
215+
if not ASK_ON_FIRST_USE:
216+
return defaults
217+
# already saved? skip
218+
if CONFIG_PATH.exists():
219+
return defaults
220+
# only prompt if interactive console
221+
if not sys.stdin or not sys.stdin.isatty():
222+
return defaults
223+
224+
avail = _discover_available(["LevelGrind", "Spin"])
225+
if not avail:
226+
return defaults
227+
228+
print("\n[ProfOak] Pick a base mode to wrap:")
229+
options = list(avail.keys()) # pretty names as discovered
230+
for i, name in enumerate(options, 1):
231+
print(f" {i}) {name}")
232+
print(f"Press ENTER for default [{defaults[0]}].")
233+
try:
234+
choice = input("> ").strip()
235+
except Exception:
236+
choice = ""
237+
if choice.isdigit():
238+
idx = int(choice) - 1
239+
if 0 <= idx < len(options):
240+
chosen = options[idx]
241+
bases = [chosen] + [b for b in defaults if _norm_name(b) != _norm_name(chosen)]
242+
_save_choice(bases)
243+
return bases
244+
# ENTER or invalid -> keep defaults (and save so we don't ask again)
245+
_save_choice(defaults)
246+
return defaults
247+
248+
# ---------------- Plugin that registers our mode ----------------
78249
class ProfOakPlugin(BotPlugin):
79250
name = "ProfOakPlugin"
80-
version = "0.2.0-alpha.1"
251+
version = "0.5.0-alpha.0"
81252
author = "HighVoltaage"
82-
description = "Adds the 'Prof Oak' mode (wrapper around Level Grind)."
253+
description = "Adds the 'Prof Oak' mode that wraps a configurable base mode (e.g., Level Grind or Spin)."
83254

84255
def get_additional_bot_modes(self) -> Iterable[type["BotMode"]]:
85-
Base = _find_level_grind_base()
256+
preferred = _get_preferred_bases()
257+
preferred = _maybe_prompt_once(preferred) # may update & persist on first use
258+
259+
Base, chosen = _resolve_base_class(preferred)
86260
if Base is None:
87-
_log_warn("[ProfOak] Could not locate Level Grind; not registering Prof Oak.")
261+
_log_warn(f"[ProfOak] Could not resolve any base from: {preferred}. Not registering Prof Oak.")
88262
return []
89263

90-
# Define the subclass *now* using the resolved base.
91-
class ProfOakMode(Base): # type: ignore
264+
pretty = chosen or getattr(Base, "__name__", "UnknownBase")
265+
_log_info(f"[ProfOak] Registering Prof Oak (wrapping base: {pretty}).")
266+
267+
class ProfOakMode(Base): # type: ignore[misc]
92268
@staticmethod
93269
def name() -> str:
94270
return "Prof Oak"
95271

96272
@staticmethod
97273
def description() -> str:
98-
return ("Prof Oak challenge wrapper. Runs Level Grind behavior while "
99-
"the ShinyQuota plugin tracks per-map/method shiny quotas and pauses when complete.")
274+
return (f"Prof Oak challenge wrapper using {pretty} behavior. "
275+
"Pairs with ShinyQuota to pause when route/method shiny quota is met.")
100276

101277
def __init__(self, *args, **kwargs):
102278
super().__init__(*args, **kwargs) # type: ignore
103-
_log_info("[ProfOak] Mode initialized (wrapping Level Grind base: %s)." % Base.__name__)
279+
_log_info(f"[ProfOak] Mode initialized (base: {pretty}).")
104280

105281
def run(self) -> "Generator": # type: ignore[override]
106-
_log_info("[ProfOak] Starting run loop (delegating to Level Grind logic).")
282+
_log_info(f"[ProfOak] Starting run loop (delegating to {pretty}).")
107283
yield from super().run() # type: ignore
108284

109-
_log_info("[ProfOak] Mode registered (base: %s)." % Base.__name__)
110285
return [ProfOakMode]

0 commit comments

Comments
 (0)