]> git.localhorst.tv Git - alttp.git/blob - resources/js/components/tracker/AutoTracking.js
basic auto tracking
[alttp.git] / resources / js / components / tracker / AutoTracking.js
1 import React from 'react';
2 import { Button } from 'react-bootstrap';
3 import { useTranslation } from 'react-i18next';
4
5 import Icon from '../common/Icon';
6 import ToggleSwitch from '../common/ToggleSwitch';
7 import {
8         IN_GAME_MODES,
9         RAM_ADDR,
10         SRAM_ADDR,
11         WRAM_ADDR,
12         buildPrizeMap,
13 } from '../../helpers/alttp-ram';
14 import { computeState, mergeStates } from '../../helpers/tracker';
15 import { useSNES } from '../../hooks/snes';
16 import { useTracker } from '../../hooks/tracker';
17
18 const AutoTracking = () => {
19         const [enabled, setEnabled] = React.useState(false);
20         const [prizeMap, setPrizeMap] = React.useState(buildPrizeMap());
21
22         const {
23                 disable: disableSNES,
24                 enable: enableSNES,
25                 openSettings,
26                 sock,
27                 status,
28         } = useSNES();
29         const { config, setState } = useTracker();
30         const { t } = useTranslation();
31
32         const enable = React.useCallback(() => {
33                 enableSNES();
34                 setEnabled(true);
35         }, []);
36
37         const disable = React.useCallback(() => {
38                 disableSNES();
39                 setEnabled(false);
40         }, []);
41
42         React.useEffect(() => {
43                 const savedSettings = localStorage.getItem('tracker.settings');
44                 if (savedSettings) {
45                         const settings = JSON.parse(savedSettings);
46                         if (settings.autoTrack) {
47                                 enable();
48                         }
49                 }
50         }, []);
51
52         const saveSettings = React.useCallback((newSettings) => {
53                 const savedSettings = localStorage.getItem('tracker.settings');
54                 const settings = savedSettings
55                         ? { ...JSON.parse(savedSettings), ...newSettings }
56                         : newSettings;
57                 localStorage.setItem('tracker.settings', JSON.stringify(settings));
58         }, []);
59
60         const toggle = React.useCallback(() => {
61                 if (enabled) {
62                         disable();
63                         saveSettings({ autoTrack: false });
64                 } else {
65                         enable();
66                         saveSettings({ autoTrack: true });
67                 }
68         }, [enabled]);
69
70         // poll game and push state
71         React.useEffect(() => {
72                 if (!enabled || status.error || !status.connected || !status.device) return;
73                 const updateState = () => {
74                         const saveStart = WRAM_ADDR.SAVE_DATA;
75                         const saveSize = SRAM_ADDR.INV_END;
76                         sock.current.readWRAM(saveStart, saveSize, (data) => {
77                                 const computed = computeState(data, prizeMap);
78                                 setState(s => mergeStates(config, s, computed));
79                         });
80                 };
81                 const fetchPrizes = () => {
82                         sock.current.readBytes(RAM_ADDR.PRIZE_MAP, 13, (prizes) => {
83                                 sock.current.readBytes(RAM_ADDR.CRYSTAL_MAP, 13, (crystals) => {
84                                         setPrizeMap(m => {
85                                                 const newMap = buildPrizeMap(prizes, crystals);
86                                                 return JSON.stringify(m) === JSON.stringify(newMap) ? m : newMap;
87                                         });
88                                 });
89                         });
90                 };
91                 const checkInGame = () => {
92                         sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => {
93                                 if (IN_GAME_MODES.includes(data[0])) {
94                                         fetchPrizes();
95                                         updateState();
96                                 }
97                         });
98                 };
99                 const timer = setInterval(checkInGame, 1000);
100                 return () => {
101                         clearInterval(timer);
102                 };
103         }, [enabled && !status.error && status.connected && status.device, config, prizeMap, sock]);
104
105         const statusMsg = React.useMemo(() => {
106                 if (!enabled) {
107                         return 'disabled';
108                 }
109                 if (status.error) {
110                         return 'error';
111                 }
112                 if (!status.connected) {
113                         return 'disconnected';
114                 }
115                 if (!status.device) {
116                         return 'no-device';
117                 }
118                 return 'tracking';
119         }, [enabled, status]);
120
121         return <div>
122                 {['disconnected', 'error', 'no-device'].includes(statusMsg) ?
123                         <Icon.WARNING
124                                 className="me-2 text-warning"
125                                 size="lg"
126                                 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device  })}
127                         />
128                 : null}
129                 {['not-applicable', 'not-in-game'].includes(statusMsg) ?
130                         <Icon.INFO
131                                 className="me-2 text-info"
132                                 size="lg"
133                                 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device  })}
134                         />
135                 : null}
136                 <Button
137                         className="me-2"
138                         onClick={openSettings}
139                         size="sm"
140                         title={t('snes.settings')}
141                         variant="outline-secondary"
142                 >
143                         <Icon.SETTINGS title="" />
144                 </Button>
145                 <ToggleSwitch
146                         onChange={toggle}
147                         title={t('autoTracking.heading')}
148                         value={enabled}
149                 />
150         </div>;
151 };
152
153 export default AutoTracking;