shortcutxl 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -26
- package/agent-docs/README.md +397 -397
- package/agent-docs/docs/compaction.md +390 -390
- package/agent-docs/docs/custom-provider.md +580 -580
- package/agent-docs/docs/extensions.md +1971 -1971
- package/agent-docs/docs/packages.md +209 -209
- package/agent-docs/docs/rpc.md +1317 -1317
- package/agent-docs/docs/sdk.md +962 -962
- package/agent-docs/docs/session.md +412 -412
- package/agent-docs/docs/termux.md +127 -127
- package/agent-docs/docs/tui.md +887 -887
- package/agent-docs/examples/README.md +25 -25
- package/agent-docs/examples/extensions/README.md +205 -205
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
- package/agent-docs/examples/extensions/bookmark.ts +50 -50
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
- package/agent-docs/examples/extensions/claude-rules.ts +86 -86
- package/agent-docs/examples/extensions/commands.ts +75 -75
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
- package/agent-docs/examples/extensions/custom-footer.ts +63 -63
- package/agent-docs/examples/extensions/custom-header.ts +73 -73
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
- package/agent-docs/examples/extensions/event-bus.ts +43 -43
- package/agent-docs/examples/extensions/file-trigger.ts +41 -41
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
- package/agent-docs/examples/extensions/handoff.ts +155 -155
- package/agent-docs/examples/extensions/hello.ts +25 -25
- package/agent-docs/examples/extensions/inline-bash.ts +94 -94
- package/agent-docs/examples/extensions/input-transform.ts +43 -43
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
- package/agent-docs/examples/extensions/message-renderer.ts +59 -59
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
- package/agent-docs/examples/extensions/modal-editor.ts +90 -90
- package/agent-docs/examples/extensions/model-status.ts +31 -31
- package/agent-docs/examples/extensions/notify.ts +55 -55
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
- package/agent-docs/examples/extensions/overlay-test.ts +159 -159
- package/agent-docs/examples/extensions/permission-gate.ts +37 -37
- package/agent-docs/examples/extensions/pirate.ts +47 -47
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
- package/agent-docs/examples/extensions/preset.ts +418 -418
- package/agent-docs/examples/extensions/protected-paths.ts +30 -30
- package/agent-docs/examples/extensions/qna.ts +122 -122
- package/agent-docs/examples/extensions/question.ts +278 -278
- package/agent-docs/examples/extensions/questionnaire.ts +440 -440
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
- package/agent-docs/examples/extensions/send-user-message.ts +97 -97
- package/agent-docs/examples/extensions/session-name.ts +27 -27
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
- package/agent-docs/examples/extensions/snake.ts +343 -343
- package/agent-docs/examples/extensions/space-invaders.ts +566 -566
- package/agent-docs/examples/extensions/ssh.ts +233 -233
- package/agent-docs/examples/extensions/status-line.ts +40 -40
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
- package/agent-docs/examples/extensions/summarize.ts +206 -206
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
- package/agent-docs/examples/extensions/todo.ts +314 -314
- package/agent-docs/examples/extensions/tool-override.ts +146 -146
- package/agent-docs/examples/extensions/tools.ts +145 -145
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
- package/agent-docs/examples/extensions/widget-placement.ts +17 -17
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
- package/agent-docs/examples/rpc-extension-ui.ts +654 -654
- package/agent-docs/examples/sdk/01-minimal.ts +22 -22
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
- package/agent-docs/examples/sdk/04-skills.ts +53 -53
- package/agent-docs/examples/sdk/05-tools.ts +56 -56
- package/agent-docs/examples/sdk/06-extensions.ts +88 -88
- package/agent-docs/examples/sdk/07-context-files.ts +40 -40
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
- package/agent-docs/examples/sdk/10-settings.ts +54 -54
- package/agent-docs/examples/sdk/11-sessions.ts +48 -48
- package/agent-docs/examples/sdk/12-full-control.ts +82 -82
- package/agent-docs/examples/sdk/README.md +144 -144
- package/agent-docs/xll-spec.md +110 -110
- package/dist/core/auth-storage.js +21 -2
- package/package.json +1 -1
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -272
- package/xll/modules/gameboy.py +241 -241
- package/xll/modules/pong.py +188 -188
- package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
- package/xll/modules/shortcut_xl/_log.py +12 -12
- package/xll/modules/shortcut_xl/_registry.py +44 -44
- package/xll/modules/stocks.py +100 -100
- /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
package/xll/modules/gameboy.py
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Game Boy emulator in Excel — type =gb_start() to play Pokemon Red!
|
|
3
|
-
|
|
4
|
-
Runs a Game Boy ROM via PyBoy at full 160x144 resolution.
|
|
5
|
-
Colors are auto-detected from the game's actual palette.
|
|
6
|
-
|
|
7
|
-
Arrow keys + Z (A) + X (B) + Enter (Start) + Backspace (Select).
|
|
8
|
-
Type =gb_stop() to end.
|
|
9
|
-
"""
|
|
10
|
-
import shortcut_xl
|
|
11
|
-
from shortcut_xl import xl_func, xl_log
|
|
12
|
-
import ctypes
|
|
13
|
-
import os
|
|
14
|
-
import time
|
|
15
|
-
|
|
16
|
-
WIDTH = 160
|
|
17
|
-
HEIGHT = 144
|
|
18
|
-
TARGET_EMU_FPS = 60 # Game Boy runs at ~60 FPS
|
|
19
|
-
|
|
20
|
-
# Column letter helper
|
|
21
|
-
def _col_letter(n):
|
|
22
|
-
s = ""
|
|
23
|
-
while n > 0:
|
|
24
|
-
n, rem = divmod(n - 1, 26)
|
|
25
|
-
s = chr(65 + rem) + s
|
|
26
|
-
return s
|
|
27
|
-
|
|
28
|
-
_GRID_RANGE = f"A3:{_col_letter(WIDTH)}{2 + HEIGHT}"
|
|
29
|
-
|
|
30
|
-
# Key mappings: Windows VK code -> PyBoy button name
|
|
31
|
-
_KEY_MAP = {
|
|
32
|
-
0x25: 'left',
|
|
33
|
-
0x26: 'up',
|
|
34
|
-
0x27: 'right',
|
|
35
|
-
0x28: 'down',
|
|
36
|
-
0x5A: 'a', # Z
|
|
37
|
-
0x58: 'b', # X
|
|
38
|
-
0x0D: 'start', # Enter
|
|
39
|
-
0x08: 'select', # Backspace
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
_state = {
|
|
43
|
-
'pyboy': None,
|
|
44
|
-
'running': False,
|
|
45
|
-
'_formatted': False,
|
|
46
|
-
'_keys_held': set(),
|
|
47
|
-
'_palette_bgr': None,
|
|
48
|
-
'_last_tick': 0.0,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
# Excel OnKey strings to suppress during gameplay
|
|
52
|
-
_ONKEY_SUPPRESS = [
|
|
53
|
-
"z", "Z", "x", "X",
|
|
54
|
-
"{LEFT}", "{RIGHT}", "{UP}", "{DOWN}",
|
|
55
|
-
"~", "{BS}",
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _suppress_keys(app):
|
|
60
|
-
for k in _ONKEY_SUPPRESS:
|
|
61
|
-
app.OnKey(k, "")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _restore_keys(app):
|
|
65
|
-
for k in _ONKEY_SUPPRESS:
|
|
66
|
-
app.OnKey(k)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _poll_keys():
|
|
70
|
-
pb = _state['pyboy']
|
|
71
|
-
if not pb:
|
|
72
|
-
return
|
|
73
|
-
currently_held = set()
|
|
74
|
-
for vk in _KEY_MAP:
|
|
75
|
-
if ctypes.windll.user32.GetAsyncKeyState(vk) & 0x8000:
|
|
76
|
-
currently_held.add(vk)
|
|
77
|
-
prev = _state['_keys_held']
|
|
78
|
-
for vk in currently_held - prev:
|
|
79
|
-
pb.button_press(_KEY_MAP[vk])
|
|
80
|
-
for vk in prev - currently_held:
|
|
81
|
-
pb.button_release(_KEY_MAP[vk])
|
|
82
|
-
_state['_keys_held'] = currently_held
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _detect_palette():
|
|
86
|
-
"""Scan screen for unique colors, build sorted palette."""
|
|
87
|
-
import numpy as np
|
|
88
|
-
screen = _state['pyboy'].screen.ndarray[:, :, :3]
|
|
89
|
-
unique = np.unique(screen.reshape(-1, 3), axis=0)
|
|
90
|
-
brightness = np.sum(unique, axis=1)
|
|
91
|
-
order = np.argsort(brightness)
|
|
92
|
-
palette = [tuple(unique[i]) for i in order]
|
|
93
|
-
_state['_palette_bgr'] = [
|
|
94
|
-
int(b) * 65536 + int(g) * 256 + int(r) for r, g, b in palette
|
|
95
|
-
]
|
|
96
|
-
# Build threshold list from midpoints between palette brightness levels
|
|
97
|
-
b_sorted = sorted(brightness)
|
|
98
|
-
thresholds = []
|
|
99
|
-
for i in range(len(b_sorted) - 1):
|
|
100
|
-
thresholds.append((b_sorted[i] + b_sorted[i + 1]) / 2.0 / 3.0) # per-channel avg
|
|
101
|
-
_state['_thresholds'] = thresholds
|
|
102
|
-
xl_log(f"gb palette: {len(palette)} colors, thresholds={thresholds}")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _grab_frame():
|
|
106
|
-
"""Fast frame grab: threshold-based, returns list-of-lists (no tuple conversion)."""
|
|
107
|
-
import numpy as np
|
|
108
|
-
screen = _state['pyboy'].screen.ndarray[:, :, 0] # R channel (GB is grayscale R=G=B)
|
|
109
|
-
thresholds = _state.get('_thresholds', [64, 128, 192])
|
|
110
|
-
|
|
111
|
-
indices = np.zeros(screen.shape, dtype=np.uint8)
|
|
112
|
-
for i, t in enumerate(thresholds):
|
|
113
|
-
indices[screen >= t] = i + 1
|
|
114
|
-
|
|
115
|
-
return indices.tolist() # list-of-lists — fast C-level conversion
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _render(grid):
|
|
119
|
-
def _paint(app):
|
|
120
|
-
app.Calculation = -4135 # xlCalculationManual
|
|
121
|
-
sheet = app.ActiveSheet
|
|
122
|
-
|
|
123
|
-
if not _state['_formatted']:
|
|
124
|
-
_state['_formatted'] = True
|
|
125
|
-
area = sheet.Range(_GRID_RANGE)
|
|
126
|
-
area.ColumnWidth = 0.75
|
|
127
|
-
area.RowHeight = 5
|
|
128
|
-
area.NumberFormat = ";;;"
|
|
129
|
-
|
|
130
|
-
# Conditional formatting from detected palette
|
|
131
|
-
area.FormatConditions.Delete()
|
|
132
|
-
for val, bgr in enumerate(_state['_palette_bgr']):
|
|
133
|
-
fc = area.FormatConditions.Add(1, 3, str(val))
|
|
134
|
-
fc.Interior.Color = bgr
|
|
135
|
-
fc.Font.Color = bgr
|
|
136
|
-
fc.StopIfTrue = False
|
|
137
|
-
|
|
138
|
-
_suppress_keys(app)
|
|
139
|
-
|
|
140
|
-
sheet.Range(_GRID_RANGE).Value = grid
|
|
141
|
-
sheet.Range("A1").Value = "GAME BOY"
|
|
142
|
-
sheet.Range("A2").Value = (
|
|
143
|
-
chr(8592) + chr(8593) + chr(8594) + chr(8595)
|
|
144
|
-
+ " Z=A X=B Enter=Start Back=Select gb_stop() to end"
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
shortcut_xl.xl_batch(_paint)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _tick():
|
|
151
|
-
if not _state['running']:
|
|
152
|
-
return
|
|
153
|
-
try:
|
|
154
|
-
pb = _state['pyboy']
|
|
155
|
-
now = time.perf_counter()
|
|
156
|
-
elapsed = now - _state['_last_tick']
|
|
157
|
-
_state['_last_tick'] = now
|
|
158
|
-
|
|
159
|
-
# Advance emulator to keep game at real speed
|
|
160
|
-
# Even if rendering is slow, the game plays at correct pace
|
|
161
|
-
frames_to_run = max(1, min(int(elapsed * TARGET_EMU_FPS), 10))
|
|
162
|
-
_poll_keys()
|
|
163
|
-
for _ in range(frames_to_run - 1):
|
|
164
|
-
pb.tick(1, False) # advance without rendering
|
|
165
|
-
pb.tick(1, True) # render last frame only
|
|
166
|
-
|
|
167
|
-
# Re-detect palette every ~2 seconds of game time
|
|
168
|
-
if pb.frame_count % 120 == 0:
|
|
169
|
-
_detect_palette()
|
|
170
|
-
_state['_formatted'] = False
|
|
171
|
-
|
|
172
|
-
grid = _grab_frame()
|
|
173
|
-
_render(grid)
|
|
174
|
-
except Exception as e:
|
|
175
|
-
xl_log(f"gameboy tick error: {e}")
|
|
176
|
-
import traceback
|
|
177
|
-
xl_log(traceback.format_exc())
|
|
178
|
-
_state['running'] = False
|
|
179
|
-
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
180
|
-
return
|
|
181
|
-
# Schedule next tick immediately — render as fast as COM allows
|
|
182
|
-
shortcut_xl.schedule_call(_tick, 0.001)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _restore_calc():
|
|
186
|
-
def _do(app):
|
|
187
|
-
_restore_keys(app)
|
|
188
|
-
app.Calculation = -4105 # xlCalculationAutomatic
|
|
189
|
-
shortcut_xl.xl_batch(_do)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
@xl_func
|
|
193
|
-
def gb_start(rom_path=""):
|
|
194
|
-
if _state['running']:
|
|
195
|
-
return "Already running! gb_stop() first"
|
|
196
|
-
|
|
197
|
-
if not rom_path or rom_path is None:
|
|
198
|
-
rom_path = "pokemon_red"
|
|
199
|
-
|
|
200
|
-
if not os.path.isfile(rom_path):
|
|
201
|
-
modules_dir = os.path.dirname(os.path.abspath(__file__))
|
|
202
|
-
roms_dir = os.path.join(os.path.dirname(modules_dir), "roms")
|
|
203
|
-
candidate = os.path.join(roms_dir, rom_path if rom_path.endswith(".gb") else rom_path + ".gb")
|
|
204
|
-
if os.path.isfile(candidate):
|
|
205
|
-
rom_path = candidate
|
|
206
|
-
|
|
207
|
-
if not os.path.isfile(rom_path):
|
|
208
|
-
return f"ROM not found: {rom_path}"
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
from pyboy import PyBoy
|
|
212
|
-
pb = PyBoy(rom_path, window='null')
|
|
213
|
-
for _ in range(200):
|
|
214
|
-
pb.tick(1, True)
|
|
215
|
-
_state['pyboy'] = pb
|
|
216
|
-
except Exception as e:
|
|
217
|
-
xl_log(f"gb_start error: {e}")
|
|
218
|
-
return f"Failed: {e}"
|
|
219
|
-
|
|
220
|
-
_detect_palette()
|
|
221
|
-
|
|
222
|
-
_state['running'] = True
|
|
223
|
-
_state['_formatted'] = False
|
|
224
|
-
_state['_keys_held'] = set()
|
|
225
|
-
_state['_last_tick'] = time.perf_counter()
|
|
226
|
-
shortcut_xl.schedule_call(_tick, 0.1)
|
|
227
|
-
return "GAME BOY!"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@xl_func
|
|
231
|
-
def gb_stop():
|
|
232
|
-
_state['running'] = False
|
|
233
|
-
pb = _state.get('pyboy')
|
|
234
|
-
if pb:
|
|
235
|
-
try:
|
|
236
|
-
pb.stop(False)
|
|
237
|
-
except Exception:
|
|
238
|
-
pass
|
|
239
|
-
_state['pyboy'] = None
|
|
240
|
-
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
241
|
-
return "Stopped"
|
|
1
|
+
"""
|
|
2
|
+
Game Boy emulator in Excel — type =gb_start() to play Pokemon Red!
|
|
3
|
+
|
|
4
|
+
Runs a Game Boy ROM via PyBoy at full 160x144 resolution.
|
|
5
|
+
Colors are auto-detected from the game's actual palette.
|
|
6
|
+
|
|
7
|
+
Arrow keys + Z (A) + X (B) + Enter (Start) + Backspace (Select).
|
|
8
|
+
Type =gb_stop() to end.
|
|
9
|
+
"""
|
|
10
|
+
import shortcut_xl
|
|
11
|
+
from shortcut_xl import xl_func, xl_log
|
|
12
|
+
import ctypes
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
WIDTH = 160
|
|
17
|
+
HEIGHT = 144
|
|
18
|
+
TARGET_EMU_FPS = 60 # Game Boy runs at ~60 FPS
|
|
19
|
+
|
|
20
|
+
# Column letter helper
|
|
21
|
+
def _col_letter(n):
|
|
22
|
+
s = ""
|
|
23
|
+
while n > 0:
|
|
24
|
+
n, rem = divmod(n - 1, 26)
|
|
25
|
+
s = chr(65 + rem) + s
|
|
26
|
+
return s
|
|
27
|
+
|
|
28
|
+
_GRID_RANGE = f"A3:{_col_letter(WIDTH)}{2 + HEIGHT}"
|
|
29
|
+
|
|
30
|
+
# Key mappings: Windows VK code -> PyBoy button name
|
|
31
|
+
_KEY_MAP = {
|
|
32
|
+
0x25: 'left',
|
|
33
|
+
0x26: 'up',
|
|
34
|
+
0x27: 'right',
|
|
35
|
+
0x28: 'down',
|
|
36
|
+
0x5A: 'a', # Z
|
|
37
|
+
0x58: 'b', # X
|
|
38
|
+
0x0D: 'start', # Enter
|
|
39
|
+
0x08: 'select', # Backspace
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_state = {
|
|
43
|
+
'pyboy': None,
|
|
44
|
+
'running': False,
|
|
45
|
+
'_formatted': False,
|
|
46
|
+
'_keys_held': set(),
|
|
47
|
+
'_palette_bgr': None,
|
|
48
|
+
'_last_tick': 0.0,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Excel OnKey strings to suppress during gameplay
|
|
52
|
+
_ONKEY_SUPPRESS = [
|
|
53
|
+
"z", "Z", "x", "X",
|
|
54
|
+
"{LEFT}", "{RIGHT}", "{UP}", "{DOWN}",
|
|
55
|
+
"~", "{BS}",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _suppress_keys(app):
|
|
60
|
+
for k in _ONKEY_SUPPRESS:
|
|
61
|
+
app.OnKey(k, "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _restore_keys(app):
|
|
65
|
+
for k in _ONKEY_SUPPRESS:
|
|
66
|
+
app.OnKey(k)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _poll_keys():
|
|
70
|
+
pb = _state['pyboy']
|
|
71
|
+
if not pb:
|
|
72
|
+
return
|
|
73
|
+
currently_held = set()
|
|
74
|
+
for vk in _KEY_MAP:
|
|
75
|
+
if ctypes.windll.user32.GetAsyncKeyState(vk) & 0x8000:
|
|
76
|
+
currently_held.add(vk)
|
|
77
|
+
prev = _state['_keys_held']
|
|
78
|
+
for vk in currently_held - prev:
|
|
79
|
+
pb.button_press(_KEY_MAP[vk])
|
|
80
|
+
for vk in prev - currently_held:
|
|
81
|
+
pb.button_release(_KEY_MAP[vk])
|
|
82
|
+
_state['_keys_held'] = currently_held
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _detect_palette():
|
|
86
|
+
"""Scan screen for unique colors, build sorted palette."""
|
|
87
|
+
import numpy as np
|
|
88
|
+
screen = _state['pyboy'].screen.ndarray[:, :, :3]
|
|
89
|
+
unique = np.unique(screen.reshape(-1, 3), axis=0)
|
|
90
|
+
brightness = np.sum(unique, axis=1)
|
|
91
|
+
order = np.argsort(brightness)
|
|
92
|
+
palette = [tuple(unique[i]) for i in order]
|
|
93
|
+
_state['_palette_bgr'] = [
|
|
94
|
+
int(b) * 65536 + int(g) * 256 + int(r) for r, g, b in palette
|
|
95
|
+
]
|
|
96
|
+
# Build threshold list from midpoints between palette brightness levels
|
|
97
|
+
b_sorted = sorted(brightness)
|
|
98
|
+
thresholds = []
|
|
99
|
+
for i in range(len(b_sorted) - 1):
|
|
100
|
+
thresholds.append((b_sorted[i] + b_sorted[i + 1]) / 2.0 / 3.0) # per-channel avg
|
|
101
|
+
_state['_thresholds'] = thresholds
|
|
102
|
+
xl_log(f"gb palette: {len(palette)} colors, thresholds={thresholds}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _grab_frame():
|
|
106
|
+
"""Fast frame grab: threshold-based, returns list-of-lists (no tuple conversion)."""
|
|
107
|
+
import numpy as np
|
|
108
|
+
screen = _state['pyboy'].screen.ndarray[:, :, 0] # R channel (GB is grayscale R=G=B)
|
|
109
|
+
thresholds = _state.get('_thresholds', [64, 128, 192])
|
|
110
|
+
|
|
111
|
+
indices = np.zeros(screen.shape, dtype=np.uint8)
|
|
112
|
+
for i, t in enumerate(thresholds):
|
|
113
|
+
indices[screen >= t] = i + 1
|
|
114
|
+
|
|
115
|
+
return indices.tolist() # list-of-lists — fast C-level conversion
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _render(grid):
|
|
119
|
+
def _paint(app):
|
|
120
|
+
app.Calculation = -4135 # xlCalculationManual
|
|
121
|
+
sheet = app.ActiveSheet
|
|
122
|
+
|
|
123
|
+
if not _state['_formatted']:
|
|
124
|
+
_state['_formatted'] = True
|
|
125
|
+
area = sheet.Range(_GRID_RANGE)
|
|
126
|
+
area.ColumnWidth = 0.75
|
|
127
|
+
area.RowHeight = 5
|
|
128
|
+
area.NumberFormat = ";;;"
|
|
129
|
+
|
|
130
|
+
# Conditional formatting from detected palette
|
|
131
|
+
area.FormatConditions.Delete()
|
|
132
|
+
for val, bgr in enumerate(_state['_palette_bgr']):
|
|
133
|
+
fc = area.FormatConditions.Add(1, 3, str(val))
|
|
134
|
+
fc.Interior.Color = bgr
|
|
135
|
+
fc.Font.Color = bgr
|
|
136
|
+
fc.StopIfTrue = False
|
|
137
|
+
|
|
138
|
+
_suppress_keys(app)
|
|
139
|
+
|
|
140
|
+
sheet.Range(_GRID_RANGE).Value = grid
|
|
141
|
+
sheet.Range("A1").Value = "GAME BOY"
|
|
142
|
+
sheet.Range("A2").Value = (
|
|
143
|
+
chr(8592) + chr(8593) + chr(8594) + chr(8595)
|
|
144
|
+
+ " Z=A X=B Enter=Start Back=Select gb_stop() to end"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
shortcut_xl.xl_batch(_paint)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _tick():
|
|
151
|
+
if not _state['running']:
|
|
152
|
+
return
|
|
153
|
+
try:
|
|
154
|
+
pb = _state['pyboy']
|
|
155
|
+
now = time.perf_counter()
|
|
156
|
+
elapsed = now - _state['_last_tick']
|
|
157
|
+
_state['_last_tick'] = now
|
|
158
|
+
|
|
159
|
+
# Advance emulator to keep game at real speed
|
|
160
|
+
# Even if rendering is slow, the game plays at correct pace
|
|
161
|
+
frames_to_run = max(1, min(int(elapsed * TARGET_EMU_FPS), 10))
|
|
162
|
+
_poll_keys()
|
|
163
|
+
for _ in range(frames_to_run - 1):
|
|
164
|
+
pb.tick(1, False) # advance without rendering
|
|
165
|
+
pb.tick(1, True) # render last frame only
|
|
166
|
+
|
|
167
|
+
# Re-detect palette every ~2 seconds of game time
|
|
168
|
+
if pb.frame_count % 120 == 0:
|
|
169
|
+
_detect_palette()
|
|
170
|
+
_state['_formatted'] = False
|
|
171
|
+
|
|
172
|
+
grid = _grab_frame()
|
|
173
|
+
_render(grid)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
xl_log(f"gameboy tick error: {e}")
|
|
176
|
+
import traceback
|
|
177
|
+
xl_log(traceback.format_exc())
|
|
178
|
+
_state['running'] = False
|
|
179
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
180
|
+
return
|
|
181
|
+
# Schedule next tick immediately — render as fast as COM allows
|
|
182
|
+
shortcut_xl.schedule_call(_tick, 0.001)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _restore_calc():
|
|
186
|
+
def _do(app):
|
|
187
|
+
_restore_keys(app)
|
|
188
|
+
app.Calculation = -4105 # xlCalculationAutomatic
|
|
189
|
+
shortcut_xl.xl_batch(_do)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@xl_func
|
|
193
|
+
def gb_start(rom_path=""):
|
|
194
|
+
if _state['running']:
|
|
195
|
+
return "Already running! gb_stop() first"
|
|
196
|
+
|
|
197
|
+
if not rom_path or rom_path is None:
|
|
198
|
+
rom_path = "pokemon_red"
|
|
199
|
+
|
|
200
|
+
if not os.path.isfile(rom_path):
|
|
201
|
+
modules_dir = os.path.dirname(os.path.abspath(__file__))
|
|
202
|
+
roms_dir = os.path.join(os.path.dirname(modules_dir), "roms")
|
|
203
|
+
candidate = os.path.join(roms_dir, rom_path if rom_path.endswith(".gb") else rom_path + ".gb")
|
|
204
|
+
if os.path.isfile(candidate):
|
|
205
|
+
rom_path = candidate
|
|
206
|
+
|
|
207
|
+
if not os.path.isfile(rom_path):
|
|
208
|
+
return f"ROM not found: {rom_path}"
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
from pyboy import PyBoy
|
|
212
|
+
pb = PyBoy(rom_path, window='null')
|
|
213
|
+
for _ in range(200):
|
|
214
|
+
pb.tick(1, True)
|
|
215
|
+
_state['pyboy'] = pb
|
|
216
|
+
except Exception as e:
|
|
217
|
+
xl_log(f"gb_start error: {e}")
|
|
218
|
+
return f"Failed: {e}"
|
|
219
|
+
|
|
220
|
+
_detect_palette()
|
|
221
|
+
|
|
222
|
+
_state['running'] = True
|
|
223
|
+
_state['_formatted'] = False
|
|
224
|
+
_state['_keys_held'] = set()
|
|
225
|
+
_state['_last_tick'] = time.perf_counter()
|
|
226
|
+
shortcut_xl.schedule_call(_tick, 0.1)
|
|
227
|
+
return "GAME BOY!"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@xl_func
|
|
231
|
+
def gb_stop():
|
|
232
|
+
_state['running'] = False
|
|
233
|
+
pb = _state.get('pyboy')
|
|
234
|
+
if pb:
|
|
235
|
+
try:
|
|
236
|
+
pb.stop(False)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
_state['pyboy'] = None
|
|
240
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
241
|
+
return "Stopped"
|