diff --git a/package.json b/package.json index ebfe617..2804228 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skilldisplay", - "version": "0.2.0", + "version": "0.3.0", "private": true, "dependencies": { "react": "^16.8.6", diff --git a/src/ACTWebsocket.js b/src/ACTWebsocket.js index 93cb01f..d4c393e 100644 --- a/src/ACTWebsocket.js +++ b/src/ACTWebsocket.js @@ -1,21 +1,30 @@ -export default function listenActWebSocket(callback) { - const url = new URLSearchParams(window.location.search) - const wsUri = `${url.get('HOST_PORT')}BeforeLogLineRead` || undefined - const ws = new WebSocket(wsUri) - ws.onerror = () => listenActWebSocket() - ws.onmessage = function (e, m) { //PING - if (e.data === '.') return ws.send('.') //PONG - - const obj = JSON.parse(e.data) - if(obj.msgtype === 'SendCharName') - { - return callback(obj.msg) - } - else if(obj.msgtype === 'Chat') - { - const code = obj.msg.substring(0, 2) //first 2 numbers POG +const handleCodes = new Set([ + '00', + '01', + '02', + '21', + '22', + '33' +]) - if(code === '21' || code === '22') return callback(obj.msg) //NetworkAbility or NetworkAoeAbility - } - } -} \ No newline at end of file +export default function listenActWebSocket( callback ) { + const url = new URLSearchParams(window.location.search) + const wsUri = `${url.get("HOST_PORT")}BeforeLogLineRead` || undefined + const ws = new WebSocket(wsUri) + ws.onerror = () => ws.close() + ws.onclose = () => setTimeout(() => { listenActWebSocket( callback ) }, 1000) + ws.onmessage = function(e, m) { + if (e.data === ".") return ws.send(".") //PING + + const obj = JSON.parse(e.data); + if (obj.msgtype === "SendCharName") { + return callback(obj.msg, null) + } else if (obj.msgtype === "Chat") { + const code = obj.msg.substring(0, 2) //first 2 numbers POG + + if (handleCodes.has(code)) return callback(obj.msg, code) //NetworkAbility or NetworkAoeAbility + } + } + + return ws +} diff --git a/src/Action.js b/src/Action.js index 1ead230..d24d979 100644 --- a/src/Action.js +++ b/src/Action.js @@ -26,14 +26,14 @@ const ogcdOverrides = new Set([ 114 //bard MB ]) -export default function Action({ action_id }) { +export default function Action({ actionId, additionalClasses }) { const [apiData, setApiData] = React.useState() React.useEffect(() => { let current = true void (async () => { const data = await ( - await fetch(`https://xivapi.com/Action/${action_id}`, { mode: 'cors' }) + await fetch(`https://xivapi.com/Action/${actionId}`, { mode: 'cors' }) ).json() if (current) { setApiData(data) @@ -43,7 +43,7 @@ export default function Action({ action_id }) { return () => { current = false } - }, [action_id]) + }, [actionId]) if (apiData === undefined || !apiData.Icon) { return null @@ -51,7 +51,7 @@ export default function Action({ action_id }) { return ( {apiData.Name diff --git a/src/App.js b/src/App.js index 7278e2d..a8a8d8a 100644 --- a/src/App.js +++ b/src/App.js @@ -2,73 +2,148 @@ import React from 'react' import listenActWebSocket from './ACTWebsocket' import './css/App.css' import Action from './Action' +import RotationContainer from './Rotation' +import ReactDOM from 'react-dom' -class App extends React.Component { - state = { - me: 0, - actionlist: [], - actionindex: 1, - lastAddedTimestamp: '', - lastAddedAction: -1, - } - - constructor(props) { - super(props) - - listenActWebSocket(this.handleLogEvent.bind(this)) - } - - handleLogEvent(data) { - if(data.charID) { - this.setState({me: data.charID}) - return - } //the ME data we need - - const me = this.state.me - - if(me === 0) return //we need data on the character first - - let log = data.split('|') - - if(parseInt(log[2],16) !== me) return //we only care about our actions - - const action = parseInt(log[4],16) - - if(action <= 8) return //things we don't care about i.e. sprint auto-attacks - - if(this.state.lastAddedTimestamp === log[1] && this.state.lastAddedAction === action) return //no double aoe stuff - - const index = this.state.actionindex +export default function App() { + // NOTE: unlike class state, useState doesn't do object merging; instead, it directly holds values + const [actionList, setActionList] = React.useState([]) + const [encounterList, setEncounterList] = React.useState([]) + + React.useEffect(() => { - this.setState((state) => { - const actionindex = (state.actionindex >= 32)?1:state.actionindex+1 - const lastAddedTimestamp = log[1] - const lastAddedAction = action - const actionlist = state.actionlist.concat({index,action}); + // These values are only used internally by the handler, + // we don't need to notify React that they were updated, + // or keep their updates synchronized with actionList updates. + // + // This means we don't have to keep them in State (or Reducer)! + let selfId + let lastTimestamp = '' + let lastAction = -1 + let currentZone = 'Unknown' + + // we need keys to persist for each push, even if we shorten the array later, + // so we store the key with the action; can't just use array index due to CSS + let lastKey = 1 + + // listenActWebSocket should be changed to return the websocket, + // and this effect should return a function that disconnects the websocket + // + // like "return () => { ws.close() }" + let ws = listenActWebSocket((data, code) => { + const openNewEncounter = (timestamp) => { + setEncounterList(encounterList => { + if(encounterList[0] && encounterList[0].rotation && encounterList[0].rotation.length <= 0) { + encounterList.shift() + } + + encounterList.unshift({ + name: currentZone, + rotation: [] + }) + + return encounterList.slice(0,3) + }) + } - return {actionindex,lastAddedTimestamp,lastAddedAction,actionlist} - }) - - setTimeout(this.purgeAction.bind(this), 10000) - } - - purgeAction() { - this.setState((state) => { - const actionlist = state.actionlist.slice(1) + if (data.charID) { + selfId = data.charID + openNewEncounter() + return + } + + switch(code) { + case '00': + const [, , refCode, , message] = data.split('|') + if(refCode === '0038' && message === 'end') openNewEncounter() + return + case '01': + const [, , , zoneName] = data.split('|') + currentZone = zoneName + return + case '02': + const [, , logCharIdHex] = data.split('|') + selfId = parseInt(logCharIdHex, 16) + openNewEncounter() + return + case '33': + const [, , , controlCode] = data.split('|') + if(controlCode === '40000012' || controlCode === '40000010') openNewEncounter() + return + default: + break + } - return {actionlist} + //if it's not any of these it must be Network(AOE)Ability, the bulk of what we want to handle + + if (selfId === undefined) return + + const [, logTimestamp, logCharIdHex, , logActionIdHex] = data.split('|') + + // microoptimization: since selfId updates way less often, + // save selfId as data.charID.toString(16), that way you don't need to + // parse logCharIdHex every time + if (parseInt(logCharIdHex, 16) !== selfId) return + + // we do a mathematical comparison with action though so can't optimize this away + const action = parseInt(logActionIdHex, 16) + + if ( + action <= 8 || + (logTimestamp === lastTimestamp && action === lastAction) + ) + return + + if((Date.now() - Date.parse(lastTimestamp)) > 120000) openNewEncounter()//last action > 120s ago + + lastTimestamp = logTimestamp + lastAction = action + + const key = (lastKey % 256) + 1 + lastKey = key + + ReactDOM.unstable_batchedUpdates(() => { + setActionList(actionList => actionList.concat({ action, key })) + setEncounterList(encounterList => { + if(!encounterList[0]) { + encounterList[0] = { + name: currentZone, + rotation: [] + } + } + + encounterList[0].rotation.push( action ) + + return encounterList + }) + }) + + // This _probably_ should be done as a separate React.useEffect instead, + // which runs as an effect whenever the value of actionList changes. + // The problem there is, it would have to detect whether the list grew + // since the last time it was called, otherwise it'd react (heh) to its own + // updates. + // + // Easier to pair it with the previous set. + setTimeout(() => { + setActionList(actionList => actionList.slice(1)) + }, 10000) }) - } - - render() { - let actions = [] - - for (const action of this.state.actionlist) { - actions.push() - } - return
{actions}
- } + return () => { ws.close() } + }, []) + + return ( +
+
+ {actionList.map(({ action, key }) => ( + + ))} +
+ {encounterList.map((encounter, i) => ( + + ))} +
+ ) } -export default App; diff --git a/src/css/Action.css b/src/css/Action.css index f5b0d27..198cfc1 100644 --- a/src/css/Action.css +++ b/src/css/Action.css @@ -1,25 +1,27 @@ -.action-icon { - animation-duration: 10s; - animation-name: action-move; - animation-timing-function: linear; - animation-fill-mode: forwards; - position: absolute; +.action-move { + animation-duration: 10s; + animation-name: action-move; + animation-timing-function: linear; + animation-fill-mode: forwards; + position: absolute; } .gcd { width: 3rem; + vertical-align: top; } .ogcd { width: 2rem; + vertical-align: top; } @keyframes action-move { - from { - transform: translateX(calc(100vw - 3rem)); - } + from { + transform: translateX(calc(100vw - 3rem)); + } - to { - transform: translateX(-3rem); - } + to { + transform: translateX(-3rem); + } } \ No newline at end of file diff --git a/src/css/App.css b/src/css/App.css index 219a8de..c1f8e47 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -1,5 +1,5 @@ .actions { - margin-top: 1em; + padding-top: 1rem; background: linear-gradient(180deg, rgba(0,0,0,0) calc(25% - 1px), rgba(255,255,255,0.5) calc(25%), @@ -11,13 +11,13 @@ rgba(255,255,255,0.5) calc(75%), rgba(0,0,0,0) calc(75% + 1px) ); - height: 3em; - - position: absolute; - top:0; - bottom: 0; - left: 0; - right: 0; - - margin: auto; + height: 3rem; + padding-bottom: 1rem; + background-color: rgba(20, 20, 20, 0.3); + display: inline-block; +} + +.container { + display: flex; + flex-direction: column; } \ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css index dfbf662..927a17f 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1,16 +1,15 @@ body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-size: 16px; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; } html { - margin: 0; - height: 100vh; - overflow: hidden; - background-color: rgba(20, 20, 20, 0.3); + margin: 0; + height: 100vh; + overflow: hidden; } \ No newline at end of file