diff --git a/client/package.json b/client/package.json index 52043bd4..99519934 100644 --- a/client/package.json +++ b/client/package.json @@ -56,7 +56,9 @@ "sortablejs": "^1.10.2", "styled-components": "^5.3.3", "ts-dedent": "^2.0.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "yjs": "^13.5.41", + "y-indexeddb": "^9.0.9" }, "scripts": { "declarations": "tsc -p tsconfig.decs.json", diff --git a/client/src/Backend/GameLogic/GameManager.ts b/client/src/Backend/GameLogic/GameManager.ts index 6041a58e..28831c0d 100644 --- a/client/src/Backend/GameLogic/GameManager.ts +++ b/client/src/Backend/GameLogic/GameManager.ts @@ -136,6 +136,7 @@ import { verifyTwitterHandle, } from '../Network/UtilityServerAPI'; import { SerializedPlugin } from '../Plugins/SerializedPlugin'; +import OtherStore from '../Storage/OtherStore'; import PersistentChunkStore from '../Storage/PersistentChunkStore'; import { easeInAnimation, emojiEaseOutAnimation } from '../Utils/Animation'; import SnarkArgsHelper from '../Utils/SnarkArgsHelper'; @@ -207,6 +208,8 @@ class GameManager extends EventEmitter { * or something like that. */ private readonly persistentChunkStore: PersistentChunkStore; + // Literally other storage stuff that people dumped into the chunk store + private readonly otherStore: OtherStore; /** * Responsible for generating snark proofs. @@ -368,6 +371,7 @@ class GameManager extends EventEmitter { contractsAPI: ContractsAPI, contractConstants: ContractConstants, persistentChunkStore: PersistentChunkStore, + otherStore: OtherStore, snarkHelper: SnarkArgsHelper, homeLocation: WorldLocation | undefined, useMockHash: boolean, @@ -474,6 +478,7 @@ class GameManager extends EventEmitter { this.contractsAPI = contractsAPI; this.persistentChunkStore = persistentChunkStore; + this.otherStore = otherStore; this.snarkHelper = snarkHelper; this.useMockHash = useMockHash; this.paused = paused; @@ -588,20 +593,25 @@ class GameManager extends EventEmitter { terminal.current?.println('Loading game data from disk...'); - const persistentChunkStore = await PersistentChunkStore.create({ account, contractAddress }); + const persistentChunkStore = new PersistentChunkStore({ account, contractAddress }); + const otherStore = await OtherStore.create({ account, contractAddress }); terminal.current?.println('Downloading data from Ethereum blockchain...'); terminal.current?.println('(the contract is very big. this may take a while)'); terminal.current?.newline(); - const initialState = await gameStateDownloader.download(contractsAPI, persistentChunkStore); - const possibleHomes = await persistentChunkStore.getHomeLocations(); + const initialState = await gameStateDownloader.download( + contractsAPI, + persistentChunkStore, + otherStore + ); + const possibleHomes = await otherStore.getHomeLocations(); terminal.current?.println(''); terminal.current?.println('Building Index...'); - await persistentChunkStore.saveTouchedPlanetIds(initialState.allTouchedPlanetIds); - await persistentChunkStore.saveRevealedCoords(initialState.allRevealedCoords); + await otherStore.saveTouchedPlanetIds(initialState.allTouchedPlanetIds); + await otherStore.saveRevealedCoords(initialState.allRevealedCoords); const knownArtifacts: Map = new Map(); @@ -632,7 +642,7 @@ class GameManager extends EventEmitter { for (const loc of possibleHomes) { if (initialState.allTouchedPlanetIds.includes(loc.hash)) { homeLocation = loc; - await persistentChunkStore.confirmHomeLocation(loc); + await otherStore.confirmHomeLocation(loc); break; } } @@ -666,6 +676,7 @@ class GameManager extends EventEmitter { contractsAPI, initialState.contractConstants, persistentChunkStore, + otherStore, snarkHelper, homeLocation, useMockHash, @@ -751,12 +762,12 @@ class GameManager extends EventEmitter { gameManager.entityStore.onTxIntent(tx); }) .on(ContractsAPIEvent.TxSubmitted, (tx: Transaction) => { - gameManager.persistentChunkStore.onEthTxSubmit(tx); + gameManager.otherStore.onEthTxSubmit(tx); gameManager.onTxSubmit(tx); }) .on(ContractsAPIEvent.TxConfirmed, async (tx: Transaction) => { if (!tx.hash) return; // this should never happen - gameManager.persistentChunkStore.onEthTxComplete(tx.hash); + gameManager.otherStore.onEthTxComplete(tx.hash); if (isUnconfirmedRevealTx(tx)) { await gameManager.hardRefreshPlanet(tx.intent.locationId); @@ -832,7 +843,7 @@ class GameManager extends EventEmitter { .on(ContractsAPIEvent.TxErrored, async (tx: Transaction) => { gameManager.entityStore.clearUnconfirmedTxIntent(tx); if (tx.hash) { - gameManager.persistentChunkStore.onEthTxComplete(tx.hash); + gameManager.otherStore.onEthTxComplete(tx.hash); } gameManager.onTxReverted(tx); }) @@ -844,7 +855,7 @@ class GameManager extends EventEmitter { gameManager.setRadius(newRadius); }); - const unconfirmedTxs = await persistentChunkStore.getUnconfirmedSubmittedEthTxs(); + const unconfirmedTxs = await otherStore.getUnconfirmedSubmittedEthTxs(); const confirmationQueue = new ThrottledConcurrentQueue({ invocationIntervalMs: 1000, maxInvocationsPerIntervalMs: 10, @@ -1590,6 +1601,9 @@ class GameManager extends EventEmitter { getChunkStore(): PersistentChunkStore { return this.persistentChunkStore; } + getOtherStore(): OtherStore { + return this.otherStore; + } /** * The perlin value at each coordinate determines the space type. There are four space @@ -1912,7 +1926,7 @@ class GameManager extends EventEmitter { ); this.terminal.current?.println(''); - await this.persistentChunkStore.addHomeLocation(planet.location); + await this.otherStore.addHomeLocation(planet.location); const getArgs = async () => { const args = await this.snarkHelper.getInitArgs( @@ -2012,7 +2026,7 @@ class GameManager extends EventEmitter { */ async addAccount(coords: WorldCoords): Promise { const loc: WorldLocation = this.locationFromCoords(coords); - await this.persistentChunkStore.addHomeLocation(loc); + await this.otherStore.addHomeLocation(loc); this.initMiningManager(coords); this.homeLocation = loc; return true; @@ -2983,7 +2997,7 @@ class GameManager extends EventEmitter { * all of the information about those planets from the blockchain. */ addNewChunk(chunk: Chunk): GameManager { - this.persistentChunkStore.addChunk(chunk, true); + this.persistentChunkStore.addChunk(chunk); for (const planetLocation of chunk.planetLocations) { this.entityStore.addPlanetLocation(planetLocation); @@ -3012,7 +3026,7 @@ class GameManager extends EventEmitter { ); const planetIdsToUpdate: LocationId[] = []; for (const chunk of chunks) { - this.persistentChunkStore.addChunk(chunk, true); + this.persistentChunkStore.addChunk(chunk); for (const planetLocation of chunk.planetLocations) { this.entityStore.addPlanetLocation(planetLocation); @@ -3236,14 +3250,14 @@ class GameManager extends EventEmitter { * Load the serialized versions of all the plugins that this player has. */ public async loadPlugins(): Promise { - return this.persistentChunkStore.loadPlugins(); + return this.otherStore.loadPlugins(); } /** * Overwrites all the saved plugins to equal the given array of plugins. */ public async savePlugins(savedPlugins: SerializedPlugin[]): Promise { - await this.persistentChunkStore.savePlugins(savedPlugins); + await this.otherStore.savePlugins(savedPlugins); } /** diff --git a/client/src/Backend/GameLogic/GameUIManager.ts b/client/src/Backend/GameLogic/GameUIManager.ts index ab90941d..764d6877 100644 --- a/client/src/Backend/GameLogic/GameUIManager.ts +++ b/client/src/Backend/GameLogic/GameUIManager.ts @@ -201,7 +201,7 @@ class GameUIManager extends EventEmitter { const uiEmitter = UIEmitter.getInstance(); const uiManager = new GameUIManager(gameManager, terminalHandle); - const modalManager = await ModalManager.create(gameManager.getChunkStore()); + const modalManager = await ModalManager.create(gameManager.getOtherStore()); uiManager.setModalManager(modalManager); diff --git a/client/src/Backend/GameLogic/InitialGameStateDownloader.tsx b/client/src/Backend/GameLogic/InitialGameStateDownloader.tsx index 675e0327..300ed60d 100644 --- a/client/src/Backend/GameLogic/InitialGameStateDownloader.tsx +++ b/client/src/Backend/GameLogic/InitialGameStateDownloader.tsx @@ -18,6 +18,7 @@ import { TerminalHandle } from '../../Frontend/Views/Terminal'; import { ContractConstants } from '../../_types/darkforest/api/ContractsAPITypes'; import { AddressTwitterMap } from '../../_types/darkforest/api/UtilityServerAPITypes'; import { tryGetAllTwitters } from '../Network/UtilityServerAPI'; +import OtherStore from '../Storage/OtherStore'; import PersistentChunkStore from '../Storage/PersistentChunkStore'; import { ContractsAPI } from './ContractsAPI'; @@ -61,7 +62,8 @@ export class InitialGameStateDownloader { async download( contractsAPI: ContractsAPI, - persistentChunkStore: PersistentChunkStore + persistentChunkStore: PersistentChunkStore, + otherStore: OtherStore ): Promise { /** * In development we use the same contract address every time we deploy, @@ -69,10 +71,10 @@ export class InitialGameStateDownloader { */ const storedTouchedPlanetIds = import.meta.env.DEV ? [] - : await persistentChunkStore.getSavedTouchedPlanetIds(); + : await otherStore.getSavedTouchedPlanetIds(); const storedRevealedCoords = import.meta.env.DEV ? [] - : await persistentChunkStore.getSavedRevealedCoords(); + : await otherStore.getSavedRevealedCoords(); this.terminal.printElement(); this.terminal.newline(); @@ -98,7 +100,9 @@ export class InitialGameStateDownloader { const arrivals: Map = new Map(); const planetVoyageIdMap: Map = new Map(); - const minedChunks = Array.from(await persistentChunkStore.allChunks()); + // Ensure that all chunks have been loaded from indexeddb + await persistentChunkStore.chunksLoaded(); + const minedChunks = Array.from(persistentChunkStore.allChunks()); const minedPlanetIds = new Set( _.flatMap(minedChunks, (c) => c.planetLocations).map((l) => l.hash) ); diff --git a/client/src/Backend/Miner/ChunkUtils.ts b/client/src/Backend/Miner/ChunkUtils.ts index f0eb8c20..982b27cd 100644 --- a/client/src/Backend/Miner/ChunkUtils.ts +++ b/client/src/Backend/Miner/ChunkUtils.ts @@ -1,5 +1,20 @@ +import type { Abstract } from '@dfdao/types'; import { Chunk, Rectangle, WorldCoords, WorldLocation } from '@dfdao/types'; -import { BucketId, ChunkId, PersistedChunk } from '../../_types/darkforest/api/ChunkStoreTypes'; +import { Map } from 'yjs'; + +/** + * one of "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + */ +type BucketId = Abstract; +type ChunkId = Abstract; + +/** + * Abstract interface shared between different types of chunk stores. Currently we have one that + * writes to IndexedDB, and one that simply throws away the data. + */ +export interface ChunkStore { + hasMinedChunk: (chunkFootprint: Rectangle) => boolean; +} /** * Deterministically assigns a bucket ID to a rectangle, based on its position and size in the @@ -27,48 +42,6 @@ export function getChunkKey(chunkLoc: Rectangle): ChunkId { `${chunkLoc.bottomLeft.y}`) as ChunkId; } -/** - * Converts from the in-game representation of a chunk to its persisted representation. - */ -export function toPersistedChunk(chunk: Chunk): PersistedChunk { - const planetLocations = chunk.planetLocations.map((location) => ({ - x: location.coords.x, - y: location.coords.y, - h: location.hash, - p: location.perlin, - b: location.biomebase, - })); - - return { - x: chunk.chunkFootprint.bottomLeft.x, - y: chunk.chunkFootprint.bottomLeft.y, - s: chunk.chunkFootprint.sideLength, - l: planetLocations, - p: chunk.perlin, - }; -} - -/** - * Converts from the persisted representation of a chunk to the in-game representation of a chunk. - */ -export const toExploredChunk = (chunk: PersistedChunk): Chunk => { - const planetLocations = chunk.l.map((location) => ({ - coords: { x: location.x, y: location.y }, - hash: location.h, - perlin: location.p, - biomebase: location.b, - })); - - return { - chunkFootprint: { - bottomLeft: { x: chunk.x, y: chunk.y }, - sideLength: chunk.s, - }, - planetLocations, - perlin: chunk.p, - }; -}; - /** * An aligned chunk is one whose corner's coordinates are multiples of its side length, and its side * length is a power of two between {@link MIN_CHUNK_SIZE} and {@link MAX_CHUNK_SIZE} inclusive. @@ -85,7 +58,7 @@ export const toExploredChunk = (chunk: PersistedChunk): Chunk => { * that the four chunks, if merged, would result in an "aligned" chunk whose side length is double * the given chunk. */ -export const getSiblingLocations = (chunkLoc: Rectangle): [Rectangle, Rectangle, Rectangle] => { +function getSiblingLocations(chunkLoc: Rectangle): [Rectangle, Rectangle, Rectangle] { const doubleSideLen = 2 * chunkLoc.sideLength; const newBottomLeftX = Math.floor(chunkLoc.bottomLeft.x / doubleSideLen) * doubleSideLen; const newBottomLeftY = Math.floor(chunkLoc.bottomLeft.y / doubleSideLen) * doubleSideLen; @@ -109,7 +82,7 @@ export const getSiblingLocations = (chunkLoc: Rectangle): [Rectangle, Rectangle, } } return [siblingLocs[0], siblingLocs[1], siblingLocs[2]]; -}; +} /** * Returns the unique aligned chunk (for definition of "aligned" see comment on @@ -134,7 +107,7 @@ export function getChunkOfSideLengthContainingPoint( * At a high level, call this function to update an efficient quadtree-like store containing all of * the chunks that a player has either mined or imported in their client. * - * More speecifically, adds the given new chunk to the given map of chunks. If the map of chunks + * More specifically, adds the given new chunk to the given map of chunks. If the map of chunks * contains all of the "sibling" chunks to this new chunk, then instead of adding it, we merge the 4 * sibling chunks, and add the merged chunk to the map and remove the existing sibling chunks. This * function is recursive, which means that if the newly created merged chunk can also be merged with @@ -149,21 +122,44 @@ export function getChunkOfSideLengthContainingPoint( * `existingChunks` map. `onAdd` will be called exactly once, whereas `onRemove` only ever be called * for sibling chunks that existed prior to this function being called. */ -export function addToChunkMap( - existingChunks: Map, - newChunk: Chunk, - onAdd?: (arg: Chunk) => void, - onRemove?: (arg: Chunk) => void, +export function processChunkInMap( + existingChunks: Map, + chunk: Chunk, + onAdd: (arg: Chunk) => void, + onRemove: (arg: Chunk) => void, maxChunkSize?: number ) { - let sideLength = newChunk.chunkFootprint.sideLength; + for ( + let clearingSideLen = 16; + clearingSideLen < chunk.chunkFootprint.sideLength; + clearingSideLen *= 2 + ) { + for (let x = 0; x < chunk.chunkFootprint.sideLength; x += clearingSideLen) { + for (let y = 0; y < chunk.chunkFootprint.sideLength; y += clearingSideLen) { + const queryChunk: Rectangle = { + bottomLeft: { + x: chunk.chunkFootprint.bottomLeft.x + x, + y: chunk.chunkFootprint.bottomLeft.y + y, + }, + sideLength: clearingSideLen, + }; + const queryChunkKey = getChunkKey(queryChunk); + const exploredChunk = existingChunks.get(queryChunkKey); + if (exploredChunk) { + onRemove(exploredChunk); + } + } + } + } + + let sideLength = chunk.chunkFootprint.sideLength; let chunkToAdd: Chunk = { chunkFootprint: { - bottomLeft: newChunk.chunkFootprint.bottomLeft, + bottomLeft: chunk.chunkFootprint.bottomLeft, sideLength, }, - planetLocations: [...newChunk.planetLocations], - perlin: newChunk.perlin, + planetLocations: [...chunk.planetLocations], + perlin: chunk.perlin, }; while (!maxChunkSize || sideLength < maxChunkSize) { const siblingLocs = getSiblingLocations(chunkToAdd.chunkFootprint); @@ -181,12 +177,8 @@ export function addToChunkMap( for (const siblingLoc of siblingLocs) { const siblingKey = getChunkKey(siblingLoc); const sibling = existingChunks.get(siblingKey); - if (onRemove !== undefined && sibling) { - onRemove(sibling); - } else { - existingChunks.delete(siblingKey); - } if (sibling) { + onRemove(sibling); planetLocations = planetLocations.concat(sibling.planetLocations); newPerlin += sibling.perlin / 4; } @@ -201,9 +193,5 @@ export function addToChunkMap( perlin: Math.floor(newPerlin * 1000) / 1000, }; } - if (onAdd !== undefined) { - onAdd(chunkToAdd); - } else { - existingChunks.set(getChunkKey(chunkToAdd.chunkFootprint), chunkToAdd); - } + onAdd(chunkToAdd); } diff --git a/client/src/Backend/Miner/MinerManager.ts b/client/src/Backend/Miner/MinerManager.ts index 4dc3f939..533e6c9d 100644 --- a/client/src/Backend/Miner/MinerManager.ts +++ b/client/src/Backend/Miner/MinerManager.ts @@ -2,9 +2,8 @@ import { perlin } from '@dfdao/hashing'; import { Chunk, PerlinConfig, Rectangle } from '@dfdao/types'; import { EventEmitter } from 'events'; import _ from 'lodash'; -import { ChunkStore } from '../../_types/darkforest/api/ChunkStoreTypes'; import { HashConfig, MinerWorkerMessage } from '../../_types/global/GlobalTypes'; -import { getChunkKey } from './ChunkUtils'; +import { ChunkStore, getChunkKey } from './ChunkUtils'; import DefaultWorker from './miner.worker.ts?worker'; import { MiningPattern } from './MiningPatterns'; diff --git a/client/src/Backend/Storage/OtherStore.ts b/client/src/Backend/Storage/OtherStore.ts new file mode 100644 index 00000000..238dda12 --- /dev/null +++ b/client/src/Backend/Storage/OtherStore.ts @@ -0,0 +1,244 @@ +import { + ClaimedCoords, + EthAddress, + LocationId, + ModalId, + ModalPosition, + PersistedTransaction, + RevealedCoords, + Transaction, + WorldLocation, +} from '@dfdao/types'; +import { IDBPDatabase, openDB } from 'idb'; +import stringify from 'json-stable-stringify'; +import { SerializedPlugin } from '../Plugins/SerializedPlugin'; + +const enum ObjectStore { + DEFAULT = 'default', + UNCONFIRMED_ETH_TXS = 'unminedEthTxs', + PLUGINS = 'plugins', + /** + * Store modal positions so that we can keep modal panes open across sessions. + */ + MODAL_POS = 'modalPositions', +} + +interface OtherStoreConfig { + db: IDBPDatabase; + contractAddress: EthAddress; + account: EthAddress; +} + +export const MODAL_POSITIONS_KEY = 'modal_positions'; + +class OtherStore { + private db: IDBPDatabase; + private confirmedTxHashes: Set; + private account: EthAddress; + private contractAddress: EthAddress; + + constructor({ db, account, contractAddress }: OtherStoreConfig) { + this.db = db; + this.confirmedTxHashes = new Set(); + this.account = account; + this.contractAddress = contractAddress; + } + + destroy(): void { + // no-op; we don't actually destroy the instance, we leave the db connection open in case we need it in the future + } + + /** + * NOTE! if you're creating a new object store, it will not be *added* to existing dark forest + * accounts. This creation code runs once per account. Therefore, if you're adding a new object + * store, and need to test it out, you must either clear the indexed db databse for this account, + * or create a brand new account. + */ + static async create({ + account, + contractAddress, + }: Omit): Promise { + const db = await openDB(`darkforest-${contractAddress}-${account}`, 1, { + upgrade(db) { + db.createObjectStore(ObjectStore.DEFAULT); + db.createObjectStore(ObjectStore.UNCONFIRMED_ETH_TXS); + db.createObjectStore(ObjectStore.PLUGINS); + db.createObjectStore(ObjectStore.MODAL_POS); + }, + }); + + const localStorageManager = new OtherStore({ db, account, contractAddress }); + + return localStorageManager; + } + + /** + * Important! This sets the key in indexed db per account and per contract. This means the same + * client can connect to multiple different dark forest contracts, with multiple different + * accounts, and the persistent storage will not overwrite data that is not relevant for the + * current configuration of the client. + */ + private async getKey( + key: string, + objStore: ObjectStore = ObjectStore.DEFAULT + ): Promise { + return await this.db.get(objStore, `${this.contractAddress}-${this.account}-${key}`); + } + + /** + * Important! This sets the key in indexed db per account and per contract. This means the same + * client can connect to multiple different dark forest contracts, with multiple different + * accounts, and the persistent storage will not overwrite data that is not relevant for the + * current configuration of the client. + */ + private async setKey( + key: string, + value: string, + objStore: ObjectStore = ObjectStore.DEFAULT + ): Promise { + await this.db.put(objStore, value, `${this.contractAddress}-${this.account}-${key}`); + } + + private async removeKey(key: string, objStore: ObjectStore = ObjectStore.DEFAULT): Promise { + await this.db.delete(objStore, `${this.contractAddress}-${this.account}-${key}`); + } + + /** + * we keep a list rather than a single location, since client/contract can + * often go out of sync on initialization - if client thinks that init + * failed but is wrong, it will prompt user to initialize with new home coords, + * which bricks the user's account. + */ + public async getHomeLocations(): Promise { + const homeLocations = await this.getKey('homeLocations'); + let parsed: WorldLocation[] = []; + if (homeLocations) { + parsed = JSON.parse(homeLocations) as WorldLocation[]; + } + + return parsed; + } + + public async addHomeLocation(location: WorldLocation): Promise { + let locationList = await this.getHomeLocations(); + if (locationList) { + locationList.push(location); + } else { + locationList = [location]; + } + locationList = Array.from(new Set(locationList)); + await this.setKey('homeLocations', stringify(locationList)); + } + + public async confirmHomeLocation(location: WorldLocation): Promise { + await this.setKey('homeLocations', stringify([location])); + } + + public async getSavedTouchedPlanetIds(): Promise { + const touchedPlanetIds = await this.getKey('touchedPlanetIds'); + + if (touchedPlanetIds) { + const parsed = JSON.parse(touchedPlanetIds) as LocationId[]; + return parsed; + } + + return []; + } + + public async getSavedRevealedCoords(): Promise { + const revealedPlanetIds = await this.getKey('revealedPlanetIds'); + + if (revealedPlanetIds) { + const parsed = JSON.parse(revealedPlanetIds); + // changed the type on 6/1/21 to include revealer field + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (parsed.length === 0 || !(parsed[0] as any).revealer) { + return []; + } + return parsed as RevealedCoords[]; + } + + return []; + } + public async getSavedClaimedCoords(): Promise { + const claimedPlanetIds = await this.getKey('claimedPlanetIds'); + + if (claimedPlanetIds) { + const parsed = JSON.parse(claimedPlanetIds); + // changed the type on 6/1/21 to include revealer field + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (parsed.length === 0 || !(parsed[0] as any).revealer) { + return []; + } + return parsed as ClaimedCoords[]; + } + + return []; + } + + public async saveTouchedPlanetIds(ids: LocationId[]) { + await this.setKey('touchedPlanetIds', stringify(ids)); + } + + public async saveRevealedCoords(revealedCoordTups: RevealedCoords[]) { + await this.setKey('revealedPlanetIds', stringify(revealedCoordTups)); + } + + public async saveClaimedCoords(claimedCoordTupps: ClaimedCoords[]) { + await this.setKey('claimedPlanetIds', stringify(claimedCoordTupps)); + } + + /** + * Whenever a transaction is submitted, it is persisted. When the transaction either fails or + * succeeds, it is un-persisted. The reason we persist submitted transactions is to be able to + * wait for them upon a fresh start of the game if you close the game before a transaction + * confirms. + */ + public async onEthTxSubmit(tx: Transaction): Promise { + // in case the tx was mined and saved already + if (!tx.hash || this.confirmedTxHashes.has(tx.hash)) return; + const ser: PersistedTransaction = { hash: tx.hash, intent: tx.intent }; + await this.db.put(ObjectStore.UNCONFIRMED_ETH_TXS, JSON.parse(JSON.stringify(ser)), tx.hash); + } + + /** + * Partner function to {@link OtherStore#onEthTxSubmit} + */ + public async onEthTxComplete(txHash: string): Promise { + this.confirmedTxHashes.add(txHash); + await this.db.delete(ObjectStore.UNCONFIRMED_ETH_TXS, txHash); + } + + public async getUnconfirmedSubmittedEthTxs(): Promise { + const ret: PersistedTransaction[] = await this.db.getAll(ObjectStore.UNCONFIRMED_ETH_TXS); + return ret; + } + + public async loadPlugins(): Promise { + const savedPlugins = await this.getKey('plugins', ObjectStore.PLUGINS); + + if (!savedPlugins) { + return []; + } + + return JSON.parse(savedPlugins) as SerializedPlugin[]; + } + + public async savePlugins(plugins: SerializedPlugin[]): Promise { + await this.setKey('plugins', JSON.stringify(plugins), ObjectStore.PLUGINS); + } + + public async saveModalPositions(modalPositions: Map): Promise { + if (!this.db.objectStoreNames.contains(ObjectStore.MODAL_POS)) return; + const serialized = JSON.stringify(Array.from(modalPositions.entries())); + await this.setKey(MODAL_POSITIONS_KEY, serialized, ObjectStore.MODAL_POS); + } + + public async loadModalPositions(): Promise> { + if (!this.db.objectStoreNames.contains(ObjectStore.MODAL_POS)) return new Map(); + const winPos = await this.getKey(MODAL_POSITIONS_KEY, ObjectStore.MODAL_POS); + return new Map(winPos ? JSON.parse(winPos) : null); + } +} + +export default OtherStore; diff --git a/client/src/Backend/Storage/PersistentChunkStore.ts b/client/src/Backend/Storage/PersistentChunkStore.ts index 609d3922..ae386c9f 100644 --- a/client/src/Backend/Storage/PersistentChunkStore.ts +++ b/client/src/Backend/Storage/PersistentChunkStore.ts @@ -1,121 +1,32 @@ -import { - Chunk, - ClaimedCoords, - DiagnosticUpdater, - EthAddress, - LocationId, - ModalId, - ModalPosition, - PersistedTransaction, - Rectangle, - RevealedCoords, - Transaction, - WorldLocation, -} from '@dfdao/types'; -import { IDBPDatabase, openDB } from 'idb'; -import stringify from 'json-stable-stringify'; -import _ from 'lodash'; +import { Chunk, DiagnosticUpdater, EthAddress, Rectangle } from '@dfdao/types'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import { Doc, Map } from 'yjs'; import { MAX_CHUNK_SIZE } from '../../Frontend/Utils/constants'; -import { ChunkId, ChunkStore, PersistedChunk } from '../../_types/darkforest/api/ChunkStoreTypes'; import { - addToChunkMap, + ChunkStore, getChunkKey, getChunkOfSideLengthContainingPoint, - toExploredChunk, - toPersistedChunk, + processChunkInMap, } from '../Miner/ChunkUtils'; -import { SerializedPlugin } from '../Plugins/SerializedPlugin'; - -const enum ObjectStore { - DEFAULT = 'default', - BOARD = 'knownBoard', - UNCONFIRMED_ETH_TXS = 'unminedEthTxs', - PLUGINS = 'plugins', - /** - * Store modal positions so that we can keep modal panes open across sessions. - */ - MODAL_POS = 'modalPositions', -} - -const enum DBActionType { - UPDATE, - DELETE, -} - -interface DBAction { - type: DBActionType; - dbKey: T; - dbValue?: Chunk; -} - -type DBTx = DBAction[]; - -interface DebouncedFunc void> { - (...args: Parameters): ReturnType | undefined; - cancel(): void; -} - -interface PersistentChunkStoreConfig { - db: IDBPDatabase; - contractAddress: EthAddress; - account: EthAddress; -} - -export const MODAL_POSITIONS_KEY = 'modal_positions'; class PersistentChunkStore implements ChunkStore { private diagnosticUpdater?: DiagnosticUpdater; - private db: IDBPDatabase; - private queuedChunkWrites: DBTx[]; - private throttledSaveChunkCacheToDisk: DebouncedFunc<() => Promise>; - private nUpdatesLastTwoMins = 0; // we save every 5s, unless this goes above 50 - private chunkMap: Map; - private confirmedTxHashes: Set; - private account: EthAddress; - private contractAddress: EthAddress; + private doc: Doc; + private chunkMap: Map; + private db: IndexeddbPersistence; + + constructor(config: { account: EthAddress; contractAddress: string }) { + this.doc = new Doc(); + this.chunkMap = this.doc.getMap('chunks'); - constructor({ db, account, contractAddress }: PersistentChunkStoreConfig) { - this.db = db; - this.queuedChunkWrites = []; - this.confirmedTxHashes = new Set(); - this.throttledSaveChunkCacheToDisk = _.throttle( - this.persistQueuedChunks, - 2000 // TODO + this.db = new IndexeddbPersistence( + `darkforest-${config.contractAddress}-${config.account}-chunks`, + this.doc ); - this.chunkMap = new Map(); - this.account = account; - this.contractAddress = contractAddress; } destroy(): void { - // no-op; we don't actually destroy the instance, we leave the db connection open in case we need it in the future - } - - /** - * NOTE! if you're creating a new object store, it will not be *added* to existing dark forest - * accounts. This creation code runs once per account. Therefore, if you're adding a new object - * store, and need to test it out, you must either clear the indexed db databse for this account, - * or create a brand new account. - */ - static async create({ - account, - contractAddress, - }: Omit): Promise { - const db = await openDB(`darkforest-${contractAddress}-${account}`, 1, { - upgrade(db) { - db.createObjectStore(ObjectStore.DEFAULT); - db.createObjectStore(ObjectStore.BOARD); - db.createObjectStore(ObjectStore.UNCONFIRMED_ETH_TXS); - db.createObjectStore(ObjectStore.PLUGINS); - db.createObjectStore(ObjectStore.MODAL_POS); - }, - }); - - const localStorageManager = new PersistentChunkStore({ db, account, contractAddress }); - - await localStorageManager.loadChunks(); - - return localStorageManager; + this.doc.destroy(); } public setDiagnosticUpdater(diagnosticUpdater?: DiagnosticUpdater) { @@ -123,179 +34,10 @@ class PersistentChunkStore implements ChunkStore { } /** - * Important! This sets the key in indexed db per account and per contract. This means the same - * client can connect to multiple different dark forest contracts, with multiple different - * accounts, and the persistent storage will not overwrite data that is not relevant for the - * current configuration of the client. + * A function to await if you need to be sure all chunks have been loaded from indexeddb */ - private async getKey( - key: string, - objStore: ObjectStore = ObjectStore.DEFAULT - ): Promise { - return await this.db.get(objStore, `${this.contractAddress}-${this.account}-${key}`); - } - - /** - * Important! This sets the key in indexed db per account and per contract. This means the same - * client can connect to multiple different dark forest contracts, with multiple different - * accounts, and the persistent storage will not overwrite data that is not relevant for the - * current configuration of the client. - */ - private async setKey( - key: string, - value: string, - objStore: ObjectStore = ObjectStore.DEFAULT - ): Promise { - await this.db.put(objStore, value, `${this.contractAddress}-${this.account}-${key}`); - } - - private async removeKey(key: string, objStore: ObjectStore = ObjectStore.DEFAULT): Promise { - await this.db.delete(objStore, `${this.contractAddress}-${this.account}-${key}`); - } - - private async bulkSetKeyInCollection( - updateChunkTxs: DBTx[], - collection: ObjectStore - ): Promise { - const tx = this.db.transaction(collection, 'readwrite'); - updateChunkTxs.forEach((updateChunkTx) => { - updateChunkTx.forEach(({ type, dbKey: key, dbValue: value }) => { - if (type === DBActionType.UPDATE) { - tx.store.put(toPersistedChunk(value as Chunk), key); - } else if (type === DBActionType.DELETE) { - tx.store.delete(key); - } - }); - }); - await tx.done; - } - - /** - * This function loads all chunks persisted in the user's storage into the game. - */ - private async loadChunks(): Promise { - // we can't bulk get all chunks, since idb will crash/hang - // we also can't assign random non-primary keys and query on ranges - // so we append a random alphanumeric character to the front of keys - // and then bulk query for keys starting with 0, then 1, then 2, etc. - // see the `getBucket` function in `ChunkUtils.ts` for more information. - const borders = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ~'; - let chunkCount = 0; - - for (let idx = 0; idx < borders.length - 1; idx += 1) { - const bucketOfChunks = await this.db.getAll( - ObjectStore.BOARD, - IDBKeyRange.bound(borders[idx], borders[idx + 1], false, true) - ); - - bucketOfChunks.forEach((chunk: PersistedChunk) => { - this.addChunk(toExploredChunk(chunk), false); - }); - - chunkCount += bucketOfChunks.length; - } - - console.log(`loaded ${chunkCount} chunks from local storage`); - } - - /** - * Rather than saving a chunk immediately after it's mined, we queue up new chunks, and - * periodically save them. This function gets all of the queued new chunks, and persists them to - * indexed db. - */ - private async persistQueuedChunks() { - const toSave = [...this.queuedChunkWrites]; // make a copy - this.queuedChunkWrites = []; - this.diagnosticUpdater && - this.diagnosticUpdater.updateDiagnostics((d) => { - d.chunkUpdates = 0; - }); - await this.bulkSetKeyInCollection(toSave, ObjectStore.BOARD); - } - - /** - * we keep a list rather than a single location, since client/contract can - * often go out of sync on initialization - if client thinks that init - * failed but is wrong, it will prompt user to initialize with new home coords, - * which bricks the user's account. - */ - public async getHomeLocations(): Promise { - const homeLocations = await this.getKey('homeLocations'); - let parsed: WorldLocation[] = []; - if (homeLocations) { - parsed = JSON.parse(homeLocations) as WorldLocation[]; - } - - return parsed; - } - - public async addHomeLocation(location: WorldLocation): Promise { - let locationList = await this.getHomeLocations(); - if (locationList) { - locationList.push(location); - } else { - locationList = [location]; - } - locationList = Array.from(new Set(locationList)); - await this.setKey('homeLocations', stringify(locationList)); - } - - public async confirmHomeLocation(location: WorldLocation): Promise { - await this.setKey('homeLocations', stringify([location])); - } - - public async getSavedTouchedPlanetIds(): Promise { - const touchedPlanetIds = await this.getKey('touchedPlanetIds'); - - if (touchedPlanetIds) { - const parsed = JSON.parse(touchedPlanetIds) as LocationId[]; - return parsed; - } - - return []; - } - - public async getSavedRevealedCoords(): Promise { - const revealedPlanetIds = await this.getKey('revealedPlanetIds'); - - if (revealedPlanetIds) { - const parsed = JSON.parse(revealedPlanetIds); - // changed the type on 6/1/21 to include revealer field - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (parsed.length === 0 || !(parsed[0] as any).revealer) { - return []; - } - return parsed as RevealedCoords[]; - } - - return []; - } - public async getSavedClaimedCoords(): Promise { - const claimedPlanetIds = await this.getKey('claimedPlanetIds'); - - if (claimedPlanetIds) { - const parsed = JSON.parse(claimedPlanetIds); - // changed the type on 6/1/21 to include revealer field - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (parsed.length === 0 || !(parsed[0] as any).revealer) { - return []; - } - return parsed as ClaimedCoords[]; - } - - return []; - } - - public async saveTouchedPlanetIds(ids: LocationId[]) { - await this.setKey('touchedPlanetIds', stringify(ids)); - } - - public async saveRevealedCoords(revealedCoordTups: RevealedCoords[]) { - await this.setKey('revealedPlanetIds', stringify(revealedCoordTups)); - } - - public async saveClaimedCoords(claimedCoordTupps: ClaimedCoords[]) { - await this.setKey('claimedPlanetIds', stringify(claimedCoordTupps)); + async chunksLoaded(): Promise { + await this.db.whenSynced; } /** @@ -309,7 +51,7 @@ class PersistentChunkStore implements ChunkStore { while (sideLength <= MAX_CHUNK_SIZE) { const testChunkLoc = getChunkOfSideLengthContainingPoint(chunkLoc.bottomLeft, sideLength); - const chunk = this.getChunkById(getChunkKey(testChunkLoc)); + const chunk = this.chunkMap.get(getChunkKey(testChunkLoc)); if (chunk) { return chunk; } @@ -323,10 +65,6 @@ class PersistentChunkStore implements ChunkStore { return !!this.getChunkByFootprint(chunkLoc); } - private getChunkById(chunkId: ChunkId): Chunk | undefined { - return this.chunkMap.get(chunkId); - } - /** * When a chunk is mined, or a chunk is imported via map import, or a chunk is loaded from * persistent storage for the first time, we need to add this chunk to the game. This function @@ -334,175 +72,31 @@ class PersistentChunkStore implements ChunkStore { * might not want to persist the chunk is if you are sure that you got it from persistent storage. * i.e. it already exists in persistent storage. */ - public addChunk(chunk: Chunk, persistChunk = true): void { + public addChunk(chunk: Chunk): void { if (this.hasMinedChunk(chunk.chunkFootprint)) { return; } - const tx: DBAction[] = []; - - if (persistChunk) { - const minedSubChunks = this.getMinedSubChunks(chunk); - for (const subChunk of minedSubChunks) { - tx.push({ - type: DBActionType.DELETE, - dbKey: getChunkKey(subChunk.chunkFootprint), - }); - } - } - - addToChunkMap( - this.chunkMap, - chunk, - (chunk) => { - tx.push({ - type: DBActionType.UPDATE, - dbKey: getChunkKey(chunk.chunkFootprint), - dbValue: chunk, - }); - }, - (chunk) => { - tx.push({ - type: DBActionType.DELETE, - dbKey: getChunkKey(chunk.chunkFootprint), - }); - }, - MAX_CHUNK_SIZE - ); - - // modify in-memory store - for (const action of tx) { - if (action.type === DBActionType.UPDATE && action.dbValue) { - this.chunkMap.set(action.dbKey, action.dbValue); - } else if (action.type === DBActionType.DELETE) { - this.chunkMap.delete(action.dbKey); - } - } + this.doc.transact(() => { + processChunkInMap(this.chunkMap, chunk, this.onAdd, this.onRemove, MAX_CHUNK_SIZE); + }); this.diagnosticUpdater?.updateDiagnostics((d) => { d.totalChunks = this.chunkMap.size; }); - - // can stop here, if we're just loading into in-memory store from storage - if (!persistChunk) { - return; - } - - this.queuedChunkWrites.push(tx); - - this.diagnosticUpdater && - this.diagnosticUpdater.updateDiagnostics((d) => { - d.chunkUpdates = this.queuedChunkWrites.length; - }); - - // save chunks every 5s if we're just starting up, or 30s once we're moving - this.recomputeSaveThrottleAfterUpdate(); - this.throttledSaveChunkCacheToDisk(); } - /** - * Returns all the mined chunks with smaller sidelength strictly contained in the chunk. - * - * TODO: move this into ChunkUtils, and also make use of it, the way that it is currently used, in - * the function named `addToChunkMap`. - */ - private getMinedSubChunks(chunk: Chunk): Chunk[] { - const ret: Chunk[] = []; - for ( - let clearingSideLen = 16; - clearingSideLen < chunk.chunkFootprint.sideLength; - clearingSideLen *= 2 - ) { - for (let x = 0; x < chunk.chunkFootprint.sideLength; x += clearingSideLen) { - for (let y = 0; y < chunk.chunkFootprint.sideLength; y += clearingSideLen) { - const queryChunk: Rectangle = { - bottomLeft: { - x: chunk.chunkFootprint.bottomLeft.x + x, - y: chunk.chunkFootprint.bottomLeft.y + y, - }, - sideLength: clearingSideLen, - }; - const queryChunkKey = getChunkKey(queryChunk); - const exploredChunk = this.getChunkById(queryChunkKey); - if (exploredChunk) { - ret.push(exploredChunk); - } - } - } - } - return ret; - } + private onAdd = (chunk: Chunk) => { + this.chunkMap.set(getChunkKey(chunk.chunkFootprint), chunk); + }; - private recomputeSaveThrottleAfterUpdate() { - this.nUpdatesLastTwoMins += 1; - if (this.nUpdatesLastTwoMins === 50) { - this.throttledSaveChunkCacheToDisk.cancel(); - this.throttledSaveChunkCacheToDisk = _.throttle(this.persistQueuedChunks, 30000); - } - setTimeout(() => { - this.nUpdatesLastTwoMins -= 1; - if (this.nUpdatesLastTwoMins === 49) { - this.throttledSaveChunkCacheToDisk.cancel(); - this.throttledSaveChunkCacheToDisk = _.throttle(this.persistQueuedChunks, 5000); - } - }, 120000); - } + private onRemove = (chunk: Chunk) => { + this.chunkMap.delete(getChunkKey(chunk.chunkFootprint)); + }; public allChunks(): Iterable { return this.chunkMap.values(); } - - /** - * Whenever a transaction is submitted, it is persisted. When the transaction either fails or - * succeeds, it is un-persisted. The reason we persist submitted transactions is to be able to - * wait for them upon a fresh start of the game if you close the game before a transaction - * confirms. - */ - public async onEthTxSubmit(tx: Transaction): Promise { - // in case the tx was mined and saved already - if (!tx.hash || this.confirmedTxHashes.has(tx.hash)) return; - const ser: PersistedTransaction = { hash: tx.hash, intent: tx.intent }; - await this.db.put(ObjectStore.UNCONFIRMED_ETH_TXS, JSON.parse(JSON.stringify(ser)), tx.hash); - } - - /** - * Partner function to {@link PersistentChunkStore#onEthTxSubmit} - */ - public async onEthTxComplete(txHash: string): Promise { - this.confirmedTxHashes.add(txHash); - await this.db.delete(ObjectStore.UNCONFIRMED_ETH_TXS, txHash); - } - - public async getUnconfirmedSubmittedEthTxs(): Promise { - const ret: PersistedTransaction[] = await this.db.getAll(ObjectStore.UNCONFIRMED_ETH_TXS); - return ret; - } - - public async loadPlugins(): Promise { - const savedPlugins = await this.getKey('plugins', ObjectStore.PLUGINS); - - if (!savedPlugins) { - return []; - } - - return JSON.parse(savedPlugins) as SerializedPlugin[]; - } - - public async savePlugins(plugins: SerializedPlugin[]): Promise { - await this.setKey('plugins', JSON.stringify(plugins), ObjectStore.PLUGINS); - } - - public async saveModalPositions(modalPositions: Map): Promise { - if (!this.db.objectStoreNames.contains(ObjectStore.MODAL_POS)) return; - const serialized = JSON.stringify(Array.from(modalPositions.entries())); - await this.setKey(MODAL_POSITIONS_KEY, serialized, ObjectStore.MODAL_POS); - } - - public async loadModalPositions(): Promise> { - if (!this.db.objectStoreNames.contains(ObjectStore.MODAL_POS)) return new Map(); - const winPos = await this.getKey(MODAL_POSITIONS_KEY, ObjectStore.MODAL_POS); - return new Map(winPos ? JSON.parse(winPos) : null); - } } export default PersistentChunkStore; diff --git a/client/src/Backend/Storage/ReaderDataStore.ts b/client/src/Backend/Storage/ReaderDataStore.ts index e2e44e1c..8d1820d2 100644 --- a/client/src/Backend/Storage/ReaderDataStore.ts +++ b/client/src/Backend/Storage/ReaderDataStore.ts @@ -75,7 +75,7 @@ class ReaderDataStore { const addressTwitterMap = await getAllTwitters(); const contractConstants = await contractsAPI.getConstants(); const persistentChunkStore = - viewer && (await PersistentChunkStore.create({ account: viewer, contractAddress })); + viewer && new PersistentChunkStore({ account: viewer, contractAddress }); const singlePlanetStore = new ReaderDataStore({ contractAddress, @@ -99,7 +99,7 @@ class ReaderDataStore { } } - private setPlanetLocationIfKnown(planet: Planet): void { + private async setPlanetLocationIfKnown(planet: Planet): Promise { let planetLocation = undefined; if (planet && isLocatable(planet)) { @@ -111,6 +111,7 @@ class ReaderDataStore { } if (this.persistentChunkStore) { + await this.persistentChunkStore.chunksLoaded(); for (const chunk of this.persistentChunkStore.allChunks()) { for (const loc of chunk.planetLocations) { if (loc.hash === planet.locationId) { @@ -147,7 +148,7 @@ class ReaderDataStore { } updatePlanetToTime(planet, [], Date.now(), contractConstants); - this.setPlanetLocationIfKnown(planet); + await this.setPlanetLocationIfKnown(planet); return planet; } diff --git a/client/src/Frontend/Game/ModalManager.ts b/client/src/Frontend/Game/ModalManager.ts index 4e1c9317..6e46f4da 100644 --- a/client/src/Frontend/Game/ModalManager.ts +++ b/client/src/Frontend/Game/ModalManager.ts @@ -1,34 +1,31 @@ import { monomitter, Monomitter } from '@dfdao/events'; import { CursorState, ModalId, ModalManagerEvent, ModalPosition, WorldCoords } from '@dfdao/types'; import { EventEmitter } from 'events'; -import type PersistentChunkStore from '../../Backend/Storage/PersistentChunkStore'; +import OtherStore from '../../Backend/Storage/OtherStore'; class ModalManager extends EventEmitter { static instance: ModalManager; private lastIndex: number; private cursorState: CursorState; - private persistentChunkStore: PersistentChunkStore; + private store: OtherStore; private modalPositions: Map; public modalPositions$: Monomitter>; public readonly activeModalId$: Monomitter; public readonly modalPositionChanged$: Monomitter; - private constructor( - persistentChunkStore: PersistentChunkStore, - modalPositions: Map - ) { + private constructor(store: OtherStore, modalPositions: Map) { super(); this.lastIndex = 0; this.activeModalId$ = monomitter(true); this.modalPositionChanged$ = monomitter(); - this.persistentChunkStore = persistentChunkStore; + this.store = store; this.modalPositions = modalPositions; } - public static async create(persistentChunkStore: PersistentChunkStore): Promise { - const modalPositions = await persistentChunkStore.loadModalPositions(); - return new ModalManager(persistentChunkStore, modalPositions); + public static async create(store: OtherStore): Promise { + const modalPositions = await store.loadModalPositions(); + return new ModalManager(store, modalPositions); } public getIndex(): number { @@ -66,13 +63,13 @@ class ModalManager extends EventEmitter { public clearModalPosition(modalId: ModalId): void { this.modalPositions.delete(modalId); - this.persistentChunkStore.saveModalPositions(this.modalPositions); + this.store.saveModalPositions(this.modalPositions); this.modalPositionChanged$.publish(modalId); } public setModalPosition(modalId: ModalId, pos: ModalPosition): void { this.modalPositions.set(modalId, pos); - this.persistentChunkStore.saveModalPositions(this.modalPositions); + this.store.saveModalPositions(this.modalPositions); this.modalPositionChanged$.publish(modalId); } diff --git a/client/src/Frontend/Pages/GameLandingPage.tsx b/client/src/Frontend/Pages/GameLandingPage.tsx index cccfd5b2..67a7bad0 100644 --- a/client/src/Frontend/Pages/GameLandingPage.tsx +++ b/client/src/Frontend/Pages/GameLandingPage.tsx @@ -430,14 +430,11 @@ export function GameLandingPage({ match, location }: RouteComponentProps<{ contr loadDiamondContract ); const isWhitelisted = await whitelist.isWhitelisted(playerAddress); - // TODO(#2329): isWhitelisted should just check the contractOwner - const adminAddress = address(await whitelist.adminAddress()); terminal.current?.println(''); terminal.current?.print('Checking if whitelisted... '); - // TODO(#2329): isWhitelisted should just check the contractOwner - if (isWhitelisted || playerAddress === adminAddress) { + if (isWhitelisted) { terminal.current?.println('Player whitelisted.'); terminal.current?.println(''); terminal.current?.println(`Welcome, player ${playerAddress}.`); diff --git a/client/src/Frontend/Utils/BrowserChecks.ts b/client/src/Frontend/Utils/BrowserChecks.ts index 0f1ba1e2..f9c3303e 100644 --- a/client/src/Frontend/Utils/BrowserChecks.ts +++ b/client/src/Frontend/Utils/BrowserChecks.ts @@ -1,5 +1,3 @@ -import _ from 'lodash'; - export const enum Incompatibility { NoIDB = 'no_idb', NotRopsten = 'not_ropsten', @@ -56,7 +54,11 @@ const checkFeatures = async (): Promise => { export const unsupportedFeatures = async (): Promise => { const features = await checkFeatures(); - return _.keys(features).filter((f: Incompatibility) => features[f]) as Incompatibility[]; + const incompats = Object.keys(features).filter( + (f: Incompatibility) => features[f] + ) as Incompatibility[]; + + return incompats; }; export const isFirefox = () => navigator.userAgent.indexOf('Firefox') > 0; diff --git a/client/src/_types/darkforest/api/ChunkStoreTypes.ts b/client/src/_types/darkforest/api/ChunkStoreTypes.ts deleted file mode 100644 index b0e93761..00000000 --- a/client/src/_types/darkforest/api/ChunkStoreTypes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Abstract, LocationId, Rectangle } from '@dfdao/types'; - -/** - * one of "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - */ -export type BucketId = Abstract; - -/** - * Don't worry about the values here. Never base code off the values here. PLEASE. - */ -export type ChunkId = Abstract; - -/** - * Chunks represent map data in some rectangle. This type represents a chunk when it is at rest in - * IndexedDB. The reason for this type's existence is that we want to reduce the amount of data we - * store on the user's computer. Shorter names hopefully means less data. - */ -export interface PersistedChunk { - x: number; // left - y: number; // bottom - s: number; // side length - l: PersistedLocation[]; - p: number; // approximate avg perlin value. used for rendering -} - -/** - * A location is a point sample of the universe. This type represents that point sample at rest when - * it is stored in IndexedDB. - */ -export interface PersistedLocation { - x: number; - y: number; - h: LocationId; - p: number; // perlin - b: number; // biomebase perlin -} - -/** - * Abstract interface shared between different types of chunk stores. Currently we have one that - * writes to IndexedDB, and one that simply throws away the data. - */ -export interface ChunkStore { - hasMinedChunk: (chunkFootprint: Rectangle) => boolean; -} diff --git a/eth/contracts/DFInitialize.sol b/eth/contracts/DFInitialize.sol index b0033910..2161c82c 100644 --- a/eth/contracts/DFInitialize.sol +++ b/eth/contracts/DFInitialize.sol @@ -179,6 +179,7 @@ contract DFInitialize is WithStorage { initializeDefaults(); initializeUpgrades(); + initializeMaxUpgrades(); gs().initializedPlanetCountByLevel = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for (uint256 i = 0; i < gameConstants().PLANET_LEVEL_THRESHOLDS.length; i += 1) { @@ -427,4 +428,32 @@ contract DFInitialize is WithStorage { defMultiplier: 100 }); } + + function initializeMaxUpgrades() public { + Upgrade[3] storage maxUpgrades = maxUpgrades(); + + maxUpgrades[uint256(UpgradeBranch.DEFENSE)] = Upgrade({ + popCapMultiplier: 250, + popGroMultiplier: 250, + rangeMultiplier: 100, + speedMultiplier: 100, + defMultiplier: 250 + }); + + maxUpgrades[uint256(UpgradeBranch.RANGE)] = Upgrade({ + popCapMultiplier: 250, + popGroMultiplier: 250, + rangeMultiplier: 250, + speedMultiplier: 100, + defMultiplier: 100 + }); + + maxUpgrades[uint256(UpgradeBranch.SPEED)] = Upgrade({ + popCapMultiplier: 250, + popGroMultiplier: 250, + rangeMultiplier: 100, + speedMultiplier: 250, + defMultiplier: 100 + }); + } } diff --git a/eth/contracts/facets/DFCoreFacet.sol b/eth/contracts/facets/DFCoreFacet.sol index f334cccd..bbcc969c 100644 --- a/eth/contracts/facets/DFCoreFacet.sol +++ b/eth/contracts/facets/DFCoreFacet.sol @@ -166,6 +166,25 @@ contract DFCoreFacet is WithStorage { return (_location, _branch); } + function upgradePlanetMax(uint256 _location, uint256 _branch) public notPaused { + // _branch specifies which of the three upgrade branches player is leveling up + // 0 improves defense + // 1 improves range + // 2 improves speed + refreshPlanet(_location); + LibPlanet.upgradePlanetMax(_location, _branch); + } + + function bulkUpgradePlanetMax(uint256[] memory _locationIds, uint256[] memory _branches) + public + notPaused + { + for (uint256 i = 0; i < _locationIds.length; i++) { + refreshPlanet(_locationIds[i]); + LibPlanet.upgradePlanetMax(_locationIds[i], _branches[i]); + } + } + function transferPlanet(uint256 _location, address _player) public notPaused { require(gameConstants().PLANET_TRANSFER_ENABLED, "planet transferring is disabled"); diff --git a/eth/contracts/facets/DFWhitelistFacet.sol b/eth/contracts/facets/DFWhitelistFacet.sol index d7d2f400..b215a763 100644 --- a/eth/contracts/facets/DFWhitelistFacet.sol +++ b/eth/contracts/facets/DFWhitelistFacet.sol @@ -26,6 +26,9 @@ contract DFWhitelistFacet is WithStorage { if (!ws().enabled) { return true; } + if (LibPermissions.contractOwner() == _addr) { + return true; + } return ws().allowedAccounts[_addr]; } diff --git a/eth/contracts/libraries/LibPlanet.sol b/eth/contracts/libraries/LibPlanet.sol index 8e3c52a9..f9aa7dc5 100644 --- a/eth/contracts/libraries/LibPlanet.sol +++ b/eth/contracts/libraries/LibPlanet.sol @@ -236,6 +236,43 @@ library LibPlanet { emit PlanetUpgraded(msg.sender, _location, _branch, upgradeBranchCurrentLevel + 1); } + function upgradePlanetMax(uint256 _location, uint256 _branch) public { + Planet storage planet = gs().planets[_location]; + require( + planet.owner == msg.sender, + "Only owner account can perform that operation on planet." + ); + uint256 planetLevel = planet.planetLevel; + require(planetLevel > 0, "Planet level is not high enough for this upgrade"); + require(_branch <= uint256(UpgradeBranch.SPEED), "Upgrade branch not valid"); + require(planet.planetType == PlanetType.PLANET, "Can only upgrade regular planets"); + require(!planet.destroyed, "planet is destroyed"); + + uint256 totalLevel = planet.upgradeState0 + planet.upgradeState1 + planet.upgradeState2; + // Only max upgrade if planet hasn't been upgraded. + if (totalLevel != 0) return; + + Upgrade memory upgrade = LibStorage.maxUpgrades()[_branch]; + // Divided for contract precision + uint256 upgradeCost = (planet.silverCap * 3) / 1000; + require( + DFTokenFacet(address(this)).getSilverBalance(msg.sender) >= upgradeCost, + "Insufficient silver to upgrade" + ); + + // do upgrade + LibGameUtils._buffPlanet(_location, upgrade); + DFTokenFacet(address(this)).burn(msg.sender, LibSilver.create(), upgradeCost); + if (_branch == uint256(UpgradeBranch.DEFENSE)) { + planet.upgradeState0 += 5; + } else if (_branch == uint256(UpgradeBranch.RANGE)) { + planet.upgradeState1 += 5; + } else if (_branch == uint256(UpgradeBranch.SPEED)) { + planet.upgradeState2 += 5; + } + emit PlanetUpgraded(msg.sender, _location, _branch, 5); + } + function checkPlayerInit( uint256 _location, uint256 _perlin, diff --git a/eth/contracts/libraries/LibStorage.sol b/eth/contracts/libraries/LibStorage.sol index 279f10bd..4143ff8a 100644 --- a/eth/contracts/libraries/LibStorage.sol +++ b/eth/contracts/libraries/LibStorage.sol @@ -186,6 +186,7 @@ library LibStorage { bytes32 constant PLANET_DEFAULT_STATS_POSITION = keccak256("darkforest.constants.planetDefaultStats"); bytes32 constant UPGRADE_POSITION = keccak256("darkforest.constants.upgrades"); + bytes32 constant MAX_UPGRADE_POSITION = keccak256("darkforest.constants.upgrades.max"); function gameStorage() internal pure returns (GameStorage storage gs) { bytes32 position = GAME_STORAGE_POSITION; @@ -228,6 +229,13 @@ library LibStorage { upgrades.slot := position } } + + function maxUpgrades() internal pure returns (Upgrade[3] storage maxUpgrades) { + bytes32 position = MAX_UPGRADE_POSITION; + assembly { + maxUpgrades.slot := position + } + } } /** @@ -263,4 +271,8 @@ contract WithStorage { function upgrades() internal pure returns (Upgrade[4][3] storage) { return LibStorage.upgrades(); } + + function maxUpgrades() internal pure returns (Upgrade[3] storage) { + return LibStorage.maxUpgrades(); + } } diff --git a/eth/test/DFSilver.ts b/eth/test/DFSilver.ts index af69cb52..4f2cd8b8 100644 --- a/eth/test/DFSilver.ts +++ b/eth/test/DFSilver.ts @@ -18,7 +18,7 @@ import { const CONTRACT_PRECISION = 1_000; -describe.only('DFSilver', async function () { +describe('DFSilver', async function () { // Bump the time out so that the test doesn't timeout during // initial fixture creation this.timeout(1000 * 60); diff --git a/eth/test/DFUpgrade.test.ts b/eth/test/DFUpgrade.test.ts index 5f5654ab..985fa84b 100644 --- a/eth/test/DFUpgrade.test.ts +++ b/eth/test/DFUpgrade.test.ts @@ -1,3 +1,4 @@ +import { UpgradeBranchName } from '@dfdao/types'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; @@ -105,6 +106,113 @@ describe('DarkForestUpgrade', function () { expect(initialPopulationGrowth).to.be.below(newPopulationGrowth); }); + it.only('should max upgrade planet stats and emit event', async function () { + const upgradeablePlanetId = LVL1_PLANET_NEBULA.id; + const silverMinePlanetId = LVL1_ASTEROID_2.id; + + await world.user1Core.initializePlayer(...makeInitArgs(SPAWN_PLANET_1)); + + // conquer silver mine and upgradeable planet + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_PLANET_NEBULA); + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_ASTEROID_2); + + await increaseBlockchainTime(); + + await world.user1Core.withdrawSilverAsteroid(silverMinePlanetId); + + await increaseBlockchainTime(); + + await world.user1Core.withdrawSilverAsteroid(silverMinePlanetId); + + await world.contract.refreshPlanet(upgradeablePlanetId); + + const planetBeforeUpgrade = await world.contract.planets(upgradeablePlanetId); + + const silverCap = planetBeforeUpgrade.silverCap.toNumber(); + + const initialSilver = (await world.contract.getSilverBalance(world.user1.address)).toNumber(); + const initialPopulationCap = planetBeforeUpgrade.populationCap; + const initialPopulationGrowth = planetBeforeUpgrade.populationGrowth; + const initialRange = planetBeforeUpgrade.range; + await expect(world.user1Core.upgradePlanetMax(upgradeablePlanetId, UpgradeBranchName.Range)) + .to.emit(world.contract, 'PlanetUpgraded') + .withArgs( + world.user1.address, + upgradeablePlanetId, + BN.from(UpgradeBranchName.Range), + BN.from(5) + ); + + const planetAfterUpgrade = await world.contract.planets(upgradeablePlanetId); + const newPopulationCap = planetAfterUpgrade.populationCap; + const newPopulationGrowth = planetAfterUpgrade.populationGrowth; + const newSilver = (await world.contract.getSilverBalance(world.user1.address)).toNumber(); + const newRange = planetAfterUpgrade.range; + + expect(newSilver).to.equal(initialSilver - (silverCap * 3) / 1000); + expect(newPopulationCap.toNumber()).to.equal(initialPopulationCap.toNumber() * 2.5); + expect(newPopulationGrowth.toNumber()).to.equal( + Math.floor(initialPopulationGrowth.toNumber() * 2.5) + ); + expect(newRange.toNumber()).to.equal(Math.floor(initialRange.toNumber() * 2.5)); + }); + + it.only('should bulk max upgrade planet stats and emit event', async function () { + const upgradeablePlanetId = LVL1_PLANET_NEBULA.id; + const otherUpgradeablePlanetId = LVL1_PLANET_DEEP_SPACE.id; + const silverMinePlanetId = LVL1_ASTEROID_2.id; + + await world.user1Core.initializePlayer(...makeInitArgs(SPAWN_PLANET_1)); + + // conquer silver mine and upgradeable planet + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_PLANET_NEBULA); + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_PLANET_DEEP_SPACE); + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_ASTEROID_2); + + // TODO: Move silver fetching logic into TestUtils + await increaseBlockchainTime(); + + await world.user1Core.withdrawSilverAsteroid(silverMinePlanetId); + + await increaseBlockchainTime(); + + await world.user1Core.withdrawSilverAsteroid(silverMinePlanetId); + + await increaseBlockchainTime(); + + await world.user1Core.withdrawSilverAsteroid(silverMinePlanetId); + + await world.contract.refreshPlanet(upgradeablePlanetId); + + const planetBeforeUpgrade = await world.contract.planets(upgradeablePlanetId); + const planetBeforeUpgrade1 = await world.contract.planets(otherUpgradeablePlanetId); + + const initialSilver = (await world.contract.getSilverBalance(world.user1.address)).toNumber(); + + const silverCap = + (planetBeforeUpgrade.silverCap.toNumber() + planetBeforeUpgrade1.silverCap.toNumber()) / 1000; + + const initialRange = planetBeforeUpgrade.range.toNumber(); + const initialSpeed = planetBeforeUpgrade1.speed.toNumber(); + + const bUTx = await world.user1Core.bulkUpgradePlanetMax( + [upgradeablePlanetId, otherUpgradeablePlanetId], + [UpgradeBranchName.Range, UpgradeBranchName.Speed] + ); + const bURct = await bUTx.wait(); + console.log(`bulk upgrade used ${bURct.gasUsed.toNumber() / 2} gas per planet`); + + const planetAfterUpgrade = await world.contract.planets(upgradeablePlanetId); + const planetAfterUpgrade1 = await world.contract.planets(otherUpgradeablePlanetId); + + const newRange = planetAfterUpgrade.range.toNumber(); + const newSpeed = planetAfterUpgrade1.speed.toNumber(); + + const newSilver = (await world.contract.getSilverBalance(world.user1.address)).toNumber(); + expect(newRange).to.equal(Math.floor(initialRange * 2.5)); + expect(newSpeed).to.equal(Math.floor(initialSpeed * 2.5)); + expect(newSilver).to.equal(initialSilver - silverCap * 3); + }); it('should reject upgrade on silver mine, ruins, silver bank, and trading post', async function () { this.timeout(0); await world.user1Core.initializePlayer(...makeInitArgs(SPAWN_PLANET_1)); diff --git a/eth/test/DFWhitelist.test.ts b/eth/test/DFWhitelist.test.ts index 5f528ae3..68ac0ffb 100644 --- a/eth/test/DFWhitelist.test.ts +++ b/eth/test/DFWhitelist.test.ts @@ -25,6 +25,14 @@ describe('DarkForestWhitelist', function () { world = await loadFixture(worldFixture); }); + it('always indicates that the admin is whitelisted', async function () { + expect(await world.contract.isWhitelisted(world.deployer.address)).to.eq(true); + }); + + it('indicates that an address is not whitelisted before it uses a key', async function () { + expect(await world.contract.isWhitelisted(world.user1.address)).to.eq(false); + }); + it('allows a user to register with a valid key', async function () { const whitelistArgs = await makeWhitelistArgs(keys[0], world.user1.address as EthAddress); await world.user1Core.useKey(...whitelistArgs); diff --git a/package-lock.json b/package-lock.json index 3a964e28..ce5bd949 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,7 +97,9 @@ "sortablejs": "^1.10.2", "styled-components": "^5.3.3", "ts-dedent": "^2.0.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.41" }, "devDependencies": { "@projectsophon/workspace": "^2.0.0", @@ -20691,6 +20693,15 @@ "ws": "*" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -21621,6 +21632,21 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.52", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.52.tgz", + "integrity": "sha512-CjxlM7UgICfN6b2OPALBXchIBiNk6jE+1g7JP8ha+dh1xKRDSYpH0WQl1+rMqCju49xUnwPG34v4CR5/rPOZhg==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/libp2p-crypto": { "version": "0.16.4", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.16.4.tgz", @@ -31995,6 +32021,21 @@ "node": ">=0.4" } }, + "node_modules/y-indexeddb": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.9.tgz", + "integrity": "sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==", + "dependencies": { + "lib0": "^0.2.35" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -32126,6 +32167,18 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yjs": { + "version": "13.5.41", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.41.tgz", + "integrity": "sha512-4eSTrrs8OeI0heXKKioRY4ag7V5Bk85Z4MeniUyown3o3y0G7G4JpAZWrZWfTp7pzw2b53GkAQWKqHsHi9j9JA==", + "dependencies": { + "lib0": "^0.2.49" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -40607,7 +40660,9 @@ "typescript": "4.7.x", "uuid": "^8.3.2", "vite": "^3.1.3", - "vitest": "^0.23.4" + "vitest": "^0.23.4", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.41" }, "dependencies": { "@rollup/plugin-commonjs": { @@ -47687,6 +47742,11 @@ "dev": true, "requires": {} }, + "isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -48390,6 +48450,14 @@ "type-check": "~0.3.2" } }, + "lib0": { + "version": "0.2.52", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.52.tgz", + "integrity": "sha512-CjxlM7UgICfN6b2OPALBXchIBiNk6jE+1g7JP8ha+dh1xKRDSYpH0WQl1+rMqCju49xUnwPG34v4CR5/rPOZhg==", + "requires": { + "isomorphic.js": "^0.2.4" + } + }, "libp2p-crypto": { "version": "0.16.4", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.16.4.tgz", @@ -55991,6 +56059,14 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true }, + "y-indexeddb": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.9.tgz", + "integrity": "sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==", + "requires": { + "lib0": "^0.2.35" + } + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -56080,6 +56156,14 @@ "fd-slicer": "~1.1.0" } }, + "yjs": { + "version": "13.5.41", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.5.41.tgz", + "integrity": "sha512-4eSTrrs8OeI0heXKKioRY4ag7V5Bk85Z4MeniUyown3o3y0G7G4JpAZWrZWfTp7pzw2b53GkAQWKqHsHi9j9JA==", + "requires": { + "lib0": "^0.2.49" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/packages/contracts/index.d.ts b/packages/contracts/index.d.ts index 8fb5872b..a9369a13 100644 --- a/packages/contracts/index.d.ts +++ b/packages/contracts/index.d.ts @@ -45,9 +45,9 @@ export declare const START_BLOCK = 0; /** * The address for the DarkForest contract. */ -export declare const CONTRACT_ADDRESS = "0x8950bab77f29E8f81e6F78AEA0a79bADD88Eeb13"; +export declare const CONTRACT_ADDRESS = "0x627a72bbE16416Ae722BA05876C5cB2dcb0Dc6BB"; /** * The address for the initalizer contract. Useful for lobbies. */ -export declare const INIT_ADDRESS = "0x500cf53555c09948f4345594F9523E7B444cD67E"; +export declare const INIT_ADDRESS = "0x1aE9623899dDc2bB42217eF985a3d98E6E7623C1"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/contracts/index.js b/packages/contracts/index.js index 29de010f..65760619 100644 --- a/packages/contracts/index.js +++ b/packages/contracts/index.js @@ -48,9 +48,9 @@ exports.START_BLOCK = 0; /** * The address for the DarkForest contract. */ -exports.CONTRACT_ADDRESS = '0x8950bab77f29E8f81e6F78AEA0a79bADD88Eeb13'; +exports.CONTRACT_ADDRESS = '0x627a72bbE16416Ae722BA05876C5cB2dcb0Dc6BB'; /** * The address for the initalizer contract. Useful for lobbies. */ -exports.INIT_ADDRESS = '0x500cf53555c09948f4345594F9523E7B444cD67E'; +exports.INIT_ADDRESS = '0x1aE9623899dDc2bB42217eF985a3d98E6E7623C1'; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/contracts/index.js.map b/packages/contracts/index.js.map index fe4d9b6b..73ae82a7 100644 --- a/packages/contracts/index.js.map +++ b/packages/contracts/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;;AAEH;;GAEG;AACU,QAAA,OAAO,GAAG,WAAW,CAAC;AACnC;;GAEG;AACU,QAAA,UAAU,GAAG,KAAK,CAAC;AAChC;;GAEG;AACU,QAAA,WAAW,GAAG,CAAC,CAAC;AAC7B;;GAEG;AACU,QAAA,gBAAgB,GAAG,4CAA4C,CAAC;AAC7E;;GAEG;AACU,QAAA,YAAY,GAAG,4CAA4C,CAAC","sourcesContent":["/**\n * This package contains deployed contract addresses, ABIs, and Typechain types\n * for the Dark Forest game.\n *\n * ## Installation\n *\n * You can install this package using [`npm`](https://www.npmjs.com) or\n * [`yarn`](https://classic.yarnpkg.com/lang/en/) by running:\n *\n * ```bash\n * npm install --save @dfdao/contracts\n * ```\n * ```bash\n * yarn add @dfdao/contracts\n * ```\n *\n * When using this in a plugin, you might want to load it with [skypack](https://www.skypack.dev)\n *\n * ```js\n * import * as contracts from 'http://cdn.skypack.dev/@dfdao/contracts'\n * ```\n *\n * ## Typechain\n *\n * The Typechain types can be found in the `typechain` directory.\n *\n * ## ABIs\n *\n * The contract ABIs can be found in the `abis` directory.\n *\n * @packageDocumentation\n */\n\n/**\n * The name of the network where these contracts are deployed.\n */\nexport const NETWORK = 'localhost';\n/**\n * The id of the network where these contracts are deployed.\n */\nexport const NETWORK_ID = 31337;\n/**\n * The block in which the DarkForest contract was initialized.\n */\nexport const START_BLOCK = 0;\n/**\n * The address for the DarkForest contract.\n */\nexport const CONTRACT_ADDRESS = '0x8950bab77f29E8f81e6F78AEA0a79bADD88Eeb13';\n/**\n * The address for the initalizer contract. Useful for lobbies.\n */\nexport const INIT_ADDRESS = '0x500cf53555c09948f4345594F9523E7B444cD67E';"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;;AAEH;;GAEG;AACU,QAAA,OAAO,GAAG,WAAW,CAAC;AACnC;;GAEG;AACU,QAAA,UAAU,GAAG,KAAK,CAAC;AAChC;;GAEG;AACU,QAAA,WAAW,GAAG,CAAC,CAAC;AAC7B;;GAEG;AACU,QAAA,gBAAgB,GAAG,4CAA4C,CAAC;AAC7E;;GAEG;AACU,QAAA,YAAY,GAAG,4CAA4C,CAAC","sourcesContent":["/**\n * This package contains deployed contract addresses, ABIs, and Typechain types\n * for the Dark Forest game.\n *\n * ## Installation\n *\n * You can install this package using [`npm`](https://www.npmjs.com) or\n * [`yarn`](https://classic.yarnpkg.com/lang/en/) by running:\n *\n * ```bash\n * npm install --save @dfdao/contracts\n * ```\n * ```bash\n * yarn add @dfdao/contracts\n * ```\n *\n * When using this in a plugin, you might want to load it with [skypack](https://www.skypack.dev)\n *\n * ```js\n * import * as contracts from 'http://cdn.skypack.dev/@dfdao/contracts'\n * ```\n *\n * ## Typechain\n *\n * The Typechain types can be found in the `typechain` directory.\n *\n * ## ABIs\n *\n * The contract ABIs can be found in the `abis` directory.\n *\n * @packageDocumentation\n */\n\n/**\n * The name of the network where these contracts are deployed.\n */\nexport const NETWORK = 'localhost';\n/**\n * The id of the network where these contracts are deployed.\n */\nexport const NETWORK_ID = 31337;\n/**\n * The block in which the DarkForest contract was initialized.\n */\nexport const START_BLOCK = 0;\n/**\n * The address for the DarkForest contract.\n */\nexport const CONTRACT_ADDRESS = '0x627a72bbE16416Ae722BA05876C5cB2dcb0Dc6BB';\n/**\n * The address for the initalizer contract. Useful for lobbies.\n */\nexport const INIT_ADDRESS = '0x1aE9623899dDc2bB42217eF985a3d98E6E7623C1';"]} \ No newline at end of file diff --git a/packages/contracts/index.ts b/packages/contracts/index.ts index 8d78358e..052b9541 100644 --- a/packages/contracts/index.ts +++ b/packages/contracts/index.ts @@ -46,8 +46,8 @@ export const START_BLOCK = 0; /** * The address for the DarkForest contract. */ -export const CONTRACT_ADDRESS = '0x8950bab77f29E8f81e6F78AEA0a79bADD88Eeb13'; +export const CONTRACT_ADDRESS = '0x627a72bbE16416Ae722BA05876C5cB2dcb0Dc6BB'; /** * The address for the initalizer contract. Useful for lobbies. */ -export const INIT_ADDRESS = '0x500cf53555c09948f4345594F9523E7B444cD67E'; \ No newline at end of file +export const INIT_ADDRESS = '0x1aE9623899dDc2bB42217eF985a3d98E6E7623C1'; \ No newline at end of file