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,
17 } from '../../helpers/alttp-ram';
18 import { useSNES } from '../../hooks/snes';
23 0x04, // fast ganon (the default and used for defeat ganon for some reason)
24 0x07, // crystals & bosses
26 0x09, // all dungeons, no aga 1
27 0x0B, // completionist
30 const GT_ENTRANCE_ID = 55;
32 const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => {
33 const [enabled, setEnabled] = React.useState(false);
34 const controls = React.useRef({
40 const [inGame, setInGame] = React.useState(false);
41 const [seedType, setSeedType] = React.useState(0);
42 const [gtCrystals, setGTCrystals] = React.useState(0);
43 const [ganonType, setGanonType] = React.useState(0);
44 const [freeItemMenu, setFreeItemMenu] = React.useState(0);
45 const [pyramidOpen, setPyramidOpen] = React.useState(false);
47 const [ownedCrystals, setOwnedCrystals] = React.useState(0);
48 const [lastEntrance, setLastEntrance] = React.useState(0);
49 const [hasEntered, setHasEntered] = React.useState(false);
50 const [basement, setBasement] = React.useState({
51 state: getGTBasementState(),
56 const [hasBigKey, setHasBigKey] = React.useState(false);
65 const { t } = useTranslation();
67 React.useEffect(() => {
73 }, [onSolve, onStart, onStop]);
75 const resetState = React.useCallback(() => {
86 state: getGTBasementState(),
94 const enable = React.useCallback(() => {
99 const disable = React.useCallback(() => {
105 React.useEffect(() => {
106 const savedSettings = localStorage.getItem('guessingGame.settings');
108 const settings = JSON.parse(savedSettings);
109 if (settings.autoTrack) {
115 const saveSettings = React.useCallback((newSettings) => {
116 const savedSettings = localStorage.getItem('guessingGame.settings');
117 const settings = savedSettings
118 ? { ...JSON.parse(savedSettings), ...newSettings }
120 localStorage.setItem('guessingGame.settings', JSON.stringify(settings));
123 const toggle = React.useCallback(() => {
126 saveSettings({ autoTrack: false });
129 saveSettings({ autoTrack: true });
134 React.useEffect(() => {
135 if (enabled && !status.error && status.connected && status.device) {
136 const checkInGame = () => {
137 sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => {
138 setInGame(IN_GAME_MODES.includes(data[0]));
142 const timer = setInterval(checkInGame, 5000);
144 clearInterval(timer);
149 }, [enabled && !status.error && status.connected && status.device]);
151 // refresh static game information
152 React.useEffect(() => {
154 sock.current.readBytes(RAM_ADDR.SEED_TYPE, 1, (data) => {
155 setSeedType(data[0]);
157 sock.current.readBytes(RAM_ADDR.GT_CRYSTALS, 1, (data) => {
158 setGTCrystals(data[0]);
160 sock.current.readBytes(RAM_ADDR.GANON_TYPE, 1, (data) => {
161 setGanonType(data[0]);
163 sock.current.readBytes(RAM_ADDR.FREE_ITEM_MENU, 1, (data) => {
164 setFreeItemMenu(data[0]);
166 sock.current.readBytes(RAM_ADDR.INIT_SRAM + SRAM_ADDR.PYRAMID_SCREEN, 1, (data) => {
167 setPyramidOpen(!!(data[0] & 0x20));
171 const applicable = React.useMemo(() => {
174 GT_TYPES.includes(ganonType) &&
176 !(freeItemMenu & 0x02);
177 }, [freeItemMenu, ganonType, gtCrystals, pyramidOpen, seedType]);
179 // update crystals information
180 React.useEffect(() => {
181 if (!applicable || !inGame || hasBigKey) return;
182 const updateCrystals = () => {
183 const crAddress = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.CRYSTALS;
184 sock.current.readWRAM(crAddress, 1, (data) => {
186 for (let i = 0; i < 7; ++i) {
187 if (data[0] & Math.pow(2, i)) {
191 setOwnedCrystals(owned);
194 // increase frequency for the last
195 const timer = setInterval(updateCrystals, ownedCrystals === gtCrystals - 1 ? 1000 : 15000);
197 clearInterval(timer);
199 }, [applicable, gtCrystals, hasBigKey, inGame, ownedCrystals, sock]);
201 // start game once all required crystals have been acquired
202 React.useEffect(() => {
203 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals || hasEntered) return;
204 controls.current.onStart();
205 const updateDungeon = () => {
206 sock.current.readWRAM(WRAM_ADDR.CURRENT_DUNGEON, 2, (data) => {
207 setLastEntrance(data[0] + (data[1] * 256));
210 const timer = setInterval(updateDungeon, 1000);
212 clearInterval(timer);
214 }, [applicable, controls, gtCrystals, hasBigKey, hasEntered, ownedCrystals]);
216 // stop game when GT has been entered
217 React.useEffect(() => {
218 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals) return;
219 if (lastEntrance === GT_ENTRANCE_ID) {
220 controls.current.onStop();
223 }, [applicable, controls, gtCrystals, hasBigKey, lastEntrance, ownedCrystals]);
226 React.useEffect(() => {
227 if (!applicable || !hasEntered || hasBigKey) return;
228 const updateGTState = () => {
229 const roomDataStart = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.ROOM_DATA_START;
230 const roomDataSize = SRAM_ADDR.ROOM_DATA_END - SRAM_ADDR.ROOM_DATA_START;
231 sock.current.readWRAM(roomDataStart, roomDataSize, (data) => {
232 const gtState = getGTBasementState(data);
233 const gtCount = countGTBasementState(gtState);
235 const cmp = compareGTBasementState(old.state, gtState);
241 torch: cmp === 'torchSeen' ? gtCount : old.torch,
248 const timer = setInterval(updateGTState, 500);
250 clearInterval(timer);
252 }, [applicable, hasBigKey, hasEntered]);
254 React.useEffect(() => {
255 if (!applicable) return;
257 const solution = basement.last === 'torch' ? basement.torch : basement.count;
258 controls.current.onSolve(solution);
260 const bkAddr = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.BIG_KEY;
261 sock.current.readWRAM(bkAddr, 1, (data) => {
262 setHasBigKey(!!(data[0] & 0x04));
265 }, [applicable, basement, controls, hasBigKey]);
267 const statusMsg = React.useMemo(() => {
274 if (!status.connected) {
275 return 'disconnected';
277 if (!status.device) {
281 return 'not-in-game';
284 return 'not-applicable';
287 }, [applicable, enabled, inGame, status]);
290 {['disconnected', 'error', 'no-device'].includes(statusMsg) ?
292 className="me-2 text-warning"
294 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
297 {['not-applicable', 'not-in-game'].includes(statusMsg) ?
299 className="me-2 text-info"
301 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
306 onClick={openSettings}
308 title={t('snes.settings')}
309 variant="outline-secondary"
311 <Icon.SETTINGS title="" />
315 title={t('autoTracking.heading')}
321 GuessingGameAutoTracking.propTypes = {
322 onSolve: PropTypes.func,
323 onStart: PropTypes.func,
324 onStop: PropTypes.func,
327 export default GuessingGameAutoTracking;