diff --git a/.env b/.env index 50d5da8f5..83ee8974a 100644 --- a/.env +++ b/.env @@ -2,5 +2,3 @@ BROWSER=none REACT_APP_API_ROOT=https://farmhand.vercel.app/ REACT_APP_NAME=$npm_package_name REACT_APP_VERSION=$npm_package_version - -REACT_APP_ENABLE_KEGS=true diff --git a/.env.development.local b/.env.development.local index 96e1e8f02..823eb23bd 100644 --- a/.env.development.local +++ b/.env.development.local @@ -1,5 +1,4 @@ REACT_APP_API_ROOT=http://localhost:3001/ -REACT_APP_ENABLE_KEGS=true REACT_APP_ENABLE_FOREST=true # Silence warnings from dev server diff --git a/api-etc/constants.js b/api-etc/constants.js index c7d8f1134..c5ae18db2 100644 --- a/api-etc/constants.js +++ b/api-etc/constants.js @@ -7,5 +7,3 @@ export const ACCEPTED_ORIGINS = new Set([ 'https://www.farmhand.life', 'https://v6p9d9t4.ssl.hwcdn.net', // itch.io's CDN that the game is served from ]) - -export const MAX_ROOM_SIZE = 25 diff --git a/api/get-market-data.js b/api/get-market-data.js index cbe8d80b8..e2f01a81e 100644 --- a/api/get-market-data.js +++ b/api/get-market-data.js @@ -5,8 +5,6 @@ const { promisify } = require('util') require('redis') require('../src/common/utils') -const { SERVER_ERRORS } = require('../src/common/constants') -const { MAX_ROOM_SIZE } = require('../api-etc/constants') // End explicit requires for serverless builds const { @@ -15,7 +13,6 @@ const { getRoomData, getRoomName, } = require('../api-etc/utils') -const { HEARTBEAT_INTERVAL_PERIOD } = require('../src/common/constants') const client = getRedisClient() @@ -23,47 +20,15 @@ const get = promisify(client.get).bind(client) const set = promisify(client.set).bind(client) module.exports = allowCors(async (req, res) => { - const { farmId = null } = req.query const roomKey = getRoomName(req) const roomData = await getRoomData(roomKey, get, set) - const { activePlayers, valueAdjustments } = roomData + const { valueAdjustments } = roomData - const now = Date.now() - - if (farmId) { - activePlayers[farmId] = now - } - - let numberOfActivePlayers = 0 - - const activePlayerIds = Object.keys(activePlayers) - - // Multiply HEARTBEAT_INTERVAL_PERIOD by some amount to account for network - // latency and other transient heartbeat delays - const evictionTimeout = HEARTBEAT_INTERVAL_PERIOD * 2.5 - - // Clean up stale activePlayers data - activePlayerIds.forEach(activePlayerId => { - const timestamp = activePlayers[activePlayerId] - const delta = now - timestamp - - if (delta > evictionTimeout) { - delete activePlayers[activePlayerId] - } else { - numberOfActivePlayers++ - } - }) - - // Note: Eviction logic (above) must happen before potentially bailing out - // here to ensure that rooms can drain appropriately. - if (farmId && numberOfActivePlayers > MAX_ROOM_SIZE) { - return res.status(403).json({ errorCode: SERVER_ERRORS.ROOM_FULL }) - } - - set(roomKey, JSON.stringify({ ...roomData, activePlayers })) + set(roomKey, JSON.stringify(roomData)) res .status(200) - .json({ activePlayers: numberOfActivePlayers, valueAdjustments }) + // TODO: activePlayers: 1 is for legacy backwards compatibility. Remove it after 10/1/2024. + .json({ valueAdjustments, activePlayers: 1 }) }) diff --git a/package-lock.json b/package-lock.json index 811c6d421..406a15aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jeremyckahn/farmhand", - "version": "1.18.11", + "version": "1.18.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@jeremyckahn/farmhand", - "version": "1.18.11", + "version": "1.18.12", "license": "GPL-2.0-or-later", "dependencies": { "@emotion/react": "^11.11.1", @@ -9789,11 +9789,11 @@ } }, "node_modules/axios": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", - "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -39639,11 +39639,11 @@ "dev": true }, "axios": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", - "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } diff --git a/package.json b/package.json index 325c8d765..caa4f46b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jeremyckahn/farmhand", - "version": "1.18.11", + "version": "1.18.12", "publishConfig": { "access": "public" }, diff --git a/src/common/constants.js b/src/common/constants.js index 7aefb0cd0..42e619e01 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -1,7 +1 @@ export const MAX_ROOM_NAME_LENGTH = 25 - -export const HEARTBEAT_INTERVAL_PERIOD = 10 * 1000 // 10 seconds - -export const SERVER_ERRORS = { - ROOM_FULL: 'ROOM_FULL', -} diff --git a/src/components/Farmhand/Farmhand.js b/src/components/Farmhand/Farmhand.js index c5a4273d6..aa7e5a6b7 100644 --- a/src/components/Farmhand/Farmhand.js +++ b/src/components/Farmhand/Farmhand.js @@ -90,23 +90,19 @@ import { import { COW_TRADE_TIMEOUT, DEFAULT_ROOM, + HEARTBEAT_INTERVAL_PERIOD, INITIAL_STORAGE_LIMIT, STAGE_TITLE_MAP, STANDARD_LOAN_AMOUNT, Z_INDEX, STANDARD_VIEW_LIST, } from '../../constants' -import { - HEARTBEAT_INTERVAL_PERIOD, - SERVER_ERRORS, -} from '../../common/constants' import { CONNECTED_TO_ROOM, LOAN_INCREASED, POSITIONS_POSTED_NOTIFICATION, RECIPE_LEARNED, RECIPES_LEARNED, - ROOM_FULL_NOTIFICATION, } from '../../templates' import { CONNECTING_TO_SERVER, @@ -638,7 +634,7 @@ export default class Farmhand extends FarmhandReducers { await this.initializeNewGame() } - this.syncToRoom().catch(errorCode => this.handleRoomSyncError(errorCode)) + this.syncToRoom() this.setState({ hasBooted: true }) } @@ -672,6 +668,7 @@ export default class Farmhand extends FarmhandReducers { const decodedRoom = decodeURIComponent(newRoom) + // NOTE: This indicates that the client should attempt to connect to the server const newIsOnline = path.startsWith('/online') if (newIsOnline !== this.state.isOnline || decodedRoom !== room) { @@ -683,7 +680,9 @@ export default class Farmhand extends FarmhandReducers { } if (isOnline !== prevState.isOnline || room !== prevState.room) { - this.syncToRoom().catch(errorCode => this.handleRoomSyncError(errorCode)) + if (newIsOnline) { + this.syncToRoom() + } if (!isOnline && typeof heartbeatTimeoutId === 'number') { clearTimeout(heartbeatTimeoutId) @@ -914,25 +913,17 @@ export default class Farmhand extends FarmhandReducers { this.state.peerRoom?.leave() - const { activePlayers, errorCode, valueAdjustments } = await getData( - endpoints.getMarketData, - { - farmId: this.state.id, - room: room, - } - ) - - if (errorCode) { - // Bail out and move control to this try's catch - throw new Error(errorCode) - } + const { valueAdjustments } = await getData(endpoints.getMarketData, { + farmId: this.state.id, + room: room, + }) this.scheduleHeartbeat() const trackerRedundancy = 4 this.setState({ - activePlayers, + activePlayers: 1, peerRoom: joinRoom( { appId: process.env.REACT_APP_NAME, @@ -951,20 +942,21 @@ export default class Farmhand extends FarmhandReducers { this.showNotification(CONNECTED_TO_ROOM`${room}`, 'success') } catch (e) { - const message = e instanceof Error ? e.message : 'Unexpected error' - // TODO: Add some reasonable fallback behavior in case the server request // fails. Possibility: Regenerate valueAdjustments and notify the user // they are offline. - if (SERVER_ERRORS[message]) { - // Bubble up the errorCode to be handled by game logic - throw message - } - this.showNotification(SERVER_ERROR, 'error') console.error(e) + + // NOTE: Syncing failed, so take the user offline + this.setState(() => { + return { + redirect: '/', + cowIdOfferedForTrade: '', + } + }) } this.setState({ @@ -973,87 +965,16 @@ export default class Farmhand extends FarmhandReducers { }) } - handleRoomSyncError(errorCode) { - const { room } = this.state - - switch (errorCode) { - case SERVER_ERRORS.ROOM_FULL: - const roomNameChunks = room.split('-') - const roomNumber = parseInt(roomNameChunks.slice(-1)[0]) // May be NaN - const nextRoomNumber = isNaN(roomNumber) ? 2 : roomNumber + 1 - const roomBaseName = roomNameChunks - .slice(0, isNaN(roomNumber) ? undefined : -1) - .join('-') - const nextRoom = `${roomBaseName}-${nextRoomNumber}` - - this.showNotification( - ROOM_FULL_NOTIFICATION`${room}${nextRoom}`, - 'warning' - ) - - this.setState(() => ({ - redirect: `/online/${encodeURIComponent(nextRoom)}`, - })) - - break - - default: - } - } - scheduleHeartbeat() { - const { heartbeatTimeoutId, id, room } = this.state + const { heartbeatTimeoutId } = this.state clearTimeout(heartbeatTimeoutId ?? -1) this.setState(() => ({ heartbeatTimeoutId: setTimeout(async () => { - try { - const { activePlayers, errorCode } = await getData( - endpoints.getMarketData, - { - farmId: id, - room, - } - ) - - if (errorCode) { - // Bail out and move control to this try's catch - throw new Error(errorCode) - } - - // If the player has been previously disconnected due to network - // flakiness (see the catch block below), attempt to rejoin the peer - // room. - const peerRoom = - this.state.peerRoom || - joinRoom( - { - appId: process.env.REACT_APP_NAME, - trackerUrls, - rtcConfig, - }, - room - ) - - this.setState(({ money }) => ({ - activePlayers, - money: moneyTotal(money, activePlayers), - peerRoom, - })) - } catch (e) { - const message = e instanceof Error ? e.message : 'Unexpected error' - - if (SERVER_ERRORS[message]) { - // Bubble up the errorCode to be handled by game logic - throw message - } - - this.showNotification(SERVER_ERROR, 'error') - - this.setState({ peerRoom: null }) - - console.error(e) - } + this.setState(({ money, activePlayers }) => ({ + activePlayers, + money: moneyTotal(money, activePlayers), + })) this.scheduleHeartbeat() }, HEARTBEAT_INTERVAL_PERIOD), @@ -1132,6 +1053,11 @@ export default class Farmhand extends FarmhandReducers { inventory ) + const { valueAdjustments } = await postData(endpoints.postDayResults, { + positions, + room, + }) + if (Object.keys(positions).length) { serverMessages.push({ message: POSITIONS_POSTED_NOTIFICATION`${'You'}${positions}`, @@ -1141,22 +1067,24 @@ export default class Farmhand extends FarmhandReducers { broadcastedPositionMessage = POSITIONS_POSTED_NOTIFICATION`${''}${positions}` } - const { valueAdjustments } = await postData(endpoints.postDayResults, { - positions, - room, - }) - nextDayState.valueAdjustments = applyPriceEvents( valueAdjustments, nextDayState.priceCrashes, nextDayState.priceSurges ) } catch (e) { + // NOTE: This will get reached when there's an issue posting data to the server. serverMessages.push({ message: SERVER_ERROR, severity: 'error', }) + // NOTE: Takes the user offline + this.setState({ + redirect: '/', + cowIdOfferedForTrade: '', + }) + console.error(e) } diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js index b9ec5e475..d28f5051e 100644 --- a/src/components/Navigation/Navigation.js +++ b/src/components/Navigation/Navigation.js @@ -154,7 +154,7 @@ const OnlineControls = ({ variant: 'contained', }} > - Active players: {integerString(activePlayers)} + Connected players: {integerString(activePlayers)} {isChatAvailable ? (