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 (
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(