1 import PropTypes from 'prop-types';
2 import React from 'react';
3 import { Button } from 'react-bootstrap';
4 import { useTranslation } from 'react-i18next';
6 import Icon from '../common/Icon';
7 import ToggleSwitch from '../common/ToggleSwitch';
9 compareGTBasementState,
12 } from '../../helpers/alttp-ram';
13 import { useSNES } from '../../hooks/snes';
15 const IN_GAME_MODES = [
17 0x06, // entering dungeon
19 0x08, // entering overworld
21 0x0A, // entering special overworld
22 0x0B, // special overworld
23 0x0E, // text/menu/map
32 0x18, // aga 2 cutscene
33 0x19, // triforce room
41 0x04, // fast ganon (the default and used for defeat ganon for some reason)
42 0x07, // crystals & bosses
44 0x09, // all dungeons, no aga 1
45 0x0B, // completionist
48 const FREE_ITEM_MENU = 0x180045;
49 const GT_CRYSTALS = 0x18019A;
50 const GANON_TYPE = 0x1801A8;
51 const SEED_TYPE = 0x180210;
52 const INIT_SRAM = 0x183000;
54 const GAME_MODE = 0x10;
55 const CURRENT_DUNGEON = 0x10E;
56 const SAVE_WRAM = 0xF000;
57 const ROOM_DATA_START = 0x000;
58 const ROOM_DATA_END = 0x140;
59 const PYRAMID_SCREEN = 0x2DB;
60 const BIG_KEYS_1 = 0x366;
61 const OWNED_CRYSTALS = 0x37A;
63 const GT_ENTRANCE_ID = 55;
65 const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => {
66 const [enabled, setEnabled] = React.useState(false);
67 const controls = React.useRef({
73 const [inGame, setInGame] = React.useState(false);
74 const [seedType, setSeedType] = React.useState(0);
75 const [gtCrystals, setGTCrystals] = React.useState(0);
76 const [ganonType, setGanonType] = React.useState(0);
77 const [freeItemMenu, setFreeItemMenu] = React.useState(0);
78 const [pyramidOpen, setPyramidOpen] = React.useState(false);
80 const [ownedCrystals, setOwnedCrystals] = React.useState(0);
81 const [lastEntrance, setLastEntrance] = React.useState(0);
82 const [hasEntered, setHasEntered] = React.useState(false);
83 const [basement, setBasement] = React.useState({
84 state: getGTBasementState(),
89 const [hasBigKey, setHasBigKey] = React.useState(false);
98 const { t } = useTranslation();
100 React.useEffect(() => {
106 }, [onSolve, onStart, onStop]);
108 const resetState = React.useCallback(() => {
117 setHasEntered(false);
119 state: getGTBasementState(),
127 const enable = React.useCallback(() => {
132 const disable = React.useCallback(() => {
138 React.useEffect(() => {
139 const savedSettings = localStorage.getItem('guessingGame.settings');
141 const settings = JSON.parse(savedSettings);
142 if (settings.autoTrack) {
148 const saveSettings = React.useCallback((newSettings) => {
149 const savedSettings = localStorage.getItem('guessingGame.settings');
150 const settings = savedSettings
151 ? { ...JSON.parse(savedSettings), ...newSettings }
153 localStorage.setItem('guessingGame.settings', JSON.stringify(settings));
156 const toggle = React.useCallback(() => {
159 saveSettings({ autoTrack: false });
162 saveSettings({ autoTrack: true });
167 React.useEffect(() => {
168 if (enabled && !status.error && status.connected && status.device) {
169 const checkInGame = () => {
170 sock.current.readWRAM(GAME_MODE, 1, (data) => {
171 setInGame(IN_GAME_MODES.includes(data[0]));
175 const timer = setInterval(checkInGame, 5000);
177 clearInterval(timer);
182 }, [enabled && !status.error && status.connected && status.device]);
184 // refresh static game information
185 React.useEffect(() => {
187 sock.current.readBytes(SEED_TYPE, 1, (data) => {
188 setSeedType(data[0]);
190 sock.current.readBytes(GT_CRYSTALS, 1, (data) => {
191 setGTCrystals(data[0]);
193 sock.current.readBytes(GANON_TYPE, 1, (data) => {
194 setGanonType(data[0]);
196 sock.current.readBytes(FREE_ITEM_MENU, 1, (data) => {
197 setFreeItemMenu(data[0]);
199 sock.current.readBytes(INIT_SRAM + PYRAMID_SCREEN, 1, (data) => {
200 setPyramidOpen(!!(data[0] & 0x20));
204 const applicable = React.useMemo(() => {
207 GT_TYPES.includes(ganonType) &&
209 !(freeItemMenu & 0x02);
210 }, [freeItemMenu, ganonType, gtCrystals, pyramidOpen, seedType]);
212 // update crystals information
213 React.useEffect(() => {
214 if (!applicable || !inGame || hasBigKey) return;
215 const updateCrystals = () => {
216 sock.current.readWRAM(SAVE_WRAM + OWNED_CRYSTALS, 1, (data) => {
218 for (let i = 0; i < 7; ++i) {
219 if (data[0] & Math.pow(2, i)) {
223 setOwnedCrystals(owned);
226 // increase frequency for the last
227 const timer = setInterval(updateCrystals, ownedCrystals === gtCrystals - 1 ? 1000 : 15000);
229 clearInterval(timer);
231 }, [applicable, gtCrystals, hasBigKey, inGame, ownedCrystals, sock]);
233 // start game once all required crystals have been acquired
234 React.useEffect(() => {
235 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals || hasEntered) return;
236 controls.current.onStart();
237 const updateDungeon = () => {
238 sock.current.readWRAM(CURRENT_DUNGEON, 2, (data) => {
239 setLastEntrance(data[0] + (data[1] * 256));
242 const timer = setInterval(updateDungeon, 1000);
244 clearInterval(timer);
246 }, [applicable, controls, gtCrystals, hasBigKey, hasEntered, ownedCrystals]);
248 // stop game when GT has been entered
249 React.useEffect(() => {
250 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals) return;
251 if (lastEntrance === GT_ENTRANCE_ID) {
252 controls.current.onStop();
255 }, [applicable, controls, gtCrystals, hasBigKey, lastEntrance, ownedCrystals]);
258 React.useEffect(() => {
259 if (!applicable || !hasEntered || hasBigKey) return;
260 const updateGTState = () => {
261 const roomDataSize = ROOM_DATA_END - ROOM_DATA_START;
262 sock.current.readWRAM(SAVE_WRAM + ROOM_DATA_START, roomDataSize, (data) => {
263 const gtState = getGTBasementState(data);
264 const gtCount = countGTBasementState(gtState);
266 const cmp = compareGTBasementState(old.state, gtState);
272 torch: cmp === 'torchSeen' ? gtCount : old.torch,
279 const timer = setInterval(updateGTState, 500);
281 clearInterval(timer);
283 }, [applicable, hasBigKey, hasEntered]);
285 React.useEffect(() => {
286 if (!applicable) return;
288 const solution = basement.last === 'torch' ? basement.torch : basement.count;
289 controls.current.onSolve(solution);
291 sock.current.readWRAM(SAVE_WRAM + BIG_KEYS_1, 1, (data) => {
292 setHasBigKey(!!(data[0] & 0x04));
295 }, [applicable, basement, controls, hasBigKey]);
297 const statusMsg = React.useMemo(() => {
304 if (!status.connected) {
305 return 'disconnected';
307 if (!status.device) {
311 return 'not-in-game';
314 return 'not-applicable';
317 }, [applicable, enabled, inGame, status]);
320 {['disconnected', 'error', 'no-device'].includes(statusMsg) ?
322 className="me-2 text-warning"
324 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
327 {['not-applicable', 'not-in-game'].includes(statusMsg) ?
329 className="me-2 text-info"
331 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
336 onClick={openSettings}
338 title={t('snes.settings')}
339 variant="outline-secondary"
341 <Icon.SETTINGS title="" />
345 title={t('autoTracking.heading')}
351 GuessingGameAutoTracking.propTypes = {
352 onSolve: PropTypes.func,
353 onStart: PropTypes.func,
354 onStop: PropTypes.func,
357 export default GuessingGameAutoTracking;