screepsmod-mongo
Version:
[](https://standardjs.com) [](https://github.com/ScreepsMods/screepsmod
625 lines (584 loc) • 25.7 kB
JavaScript
const q = require('q')
const EventEmitter = require('events').EventEmitter
const { MongoClient, ObjectId } = require('mongodb')
const Redis = require('redis')
const fs = require('fs')
const path = require('path')
const DATABASE_VERSION = 9
let C
module.exports = function (config) {
Object.assign(exports, config.common.storage)
C = config.common.constants
return function storageConnect () {
if (exports._connected) {
return q.when()
}
let uri
if (config.mongo.uri) {
uri = config.mongo.uri
delete config.mongo.uri
} else {
uri = `mongodb://${config.mongo.host}:${config.mongo.port}/${config.mongo.database}`
}
delete config.mongo.host
delete config.mongo.port
delete config.mongo.database
const redis = Redis.createClient(config.redis)
const pub = Redis.createClient(config.redis)
const sub = Redis.createClient(config.redis)
config.redis.clients = { redis, pub, sub }
const mongo = q.ninvoke(MongoClient, 'connect', uri, Object.assign({ promiseLibrary: Promise, useUnifiedTopology: true }, config.mongo))
.then(client => {
config.mongo.client = client
return client.db()
})
.then(db => {
function wrapCollection (collection, cname) {
const wrap = {}
function keyToId (obj) {
const idRegex = /^[a-f0-9]{24}$/
if (obj instanceof Array) return obj.map(keyToId)
if (obj._id && obj._id.$in) {
return Object.assign({}, obj, {
_id: {
$in: obj._id.$in.map(i => {
if (typeof i === 'string' && i.match(idRegex)) {
i = new ObjectId(i)
}
return i
})
}
})
}
if (typeof obj._id === 'string' && obj._id.match(idRegex)) {
return Object.assign({}, obj, { _id: new ObjectId(obj._id) })
}
return obj
}
function idToKey (obj) {
if (obj instanceof Array) return obj.map(idToKey)
if (obj && obj._id) {
obj._id = obj._id.toString()
}
return obj
}
function patchLokiOps (query, depth = 5) {
if (!depth) return
for (const k in query) {
const v = query[k]
if (k === '$aeq') {
delete query[k]
query.$eq = v
}
if (k === '$regex') {
// https://github.com/screeps/backend-local/blob/7520c8c7e6a443ad955d25e064dbd151a909d8cb/lib/cronjobs.js#L574
// const centralRooms = await db['rooms'].find({_id: {$regex: '^[WE]\d*5[NS]\d*5$'}, status: {$ne: 'out of borders'}});
//
// regex is not properly escaped resulting in only a subset of possible sectors actually returning
if (v === '^[WE]d*5[NS]d*5$') {
query.$regex = v.replace(/\]d\*/g, ']\\d*')
// https://github.com/screeps/backend-local/blob/7520c8c7e6a443ad955d25e064dbd151a909d8cb/lib/cronjobs.js#L393
// https://github.com/screeps/backend-local/blob/7520c8c7e6a443ad955d25e064dbd151a909d8cb/lib/strongholds.js#L132
//
// ignore properly escaped sector regex queries
} else if (typeof v === 'string' && v.match(/\^[EW]\d*\\d[NS]\d*\\d\$/g) === null && v.match(/\^\[[EW]{2}\]\\d\*5\[[NS]{2}\]\\d\*5\$/g) === null) {
// default regex escape fix for loki regex queries to work with mongo regex queries
query.$regex = v.replace(/\\{1,2}/g, '\\\\')
} else {
query.$regex = v
}
}
if (typeof v === 'object') {
patchLokiOps(v, depth - 1)
}
}
}
;['find', 'findOne', 'findEx', 'by', 'count', 'ensureIndex', 'remove', 'insert', 'update'].forEach(cmethod => {
wrap[cmethod] = (...a) => {
let method = cmethod
try {
const orig = a[0]
if (typeof a[0] === 'object') {
a[0] = keyToId(a[0])
patchLokiOps(a[0])
}
if (method === 'update') {
a[2] = a[2] || {}
if (a[2].multi !== false && Object.keys(a[1]).reduce((l, v) => l && v[0] === '$', true)) {
a[2].multi = true
}
if (a[1].$merge) {
const merge = a[1].$merge
delete a[1].$merge
const flat = (obj, stack = []) => {
const ret = {}
if (typeof obj === 'object' && !Array.isArray(obj)) {
Object.entries(obj).forEach(([k, v]) => {
Object.assign(ret, flat(v, [...stack, k]))
})
} else if (stack.length) {
ret[stack.join('.')] = obj
} else {
return obj
}
return ret
}
a[1].$set = flat(merge)
}
}
const ex = method === 'findEx'
if (method === 'find' && a[1]) {
a[1] = { projection: a[1] }
}
if (ex) {
method = 'find'
a[1].skip = a[1].offset
delete a[1].offset
}
if (method.slice(0, 6) === 'insert') method = Array.isArray(a[0]) ? 'insertMany' : 'insertOne'
let chain = collection[method](...a)
if (method === 'insertOne') {
chain = chain.then((n) => {
orig._id = n.insertedId
return orig
})
}
if (method === 'insertMany') {
chain = chain.then((n) => {
orig.forEach((o, i) => (o._id = n.insertedIds[i]))
return orig
})
}
if (method === 'update') {
chain = chain.then((n) => Object.assign(n, { modified: n.result.nModified }))
}
if (method === 'find') {
chain = q.ninvoke(chain, 'toArray')
}
chain = chain.then(idToKey)
.catch(e => {
console.error('DBERR', e, ex, a)
console.log('DBERR', e.stack)
})
return q(chain)
} catch (e) {
console.error('DBERR', e)
console.log('DBERR', e.stack)
return q(Promise.reject(e))
}
}
})
wrap.drop = (...a) => {
return q.ninvoke(collection, 'drop', ...a).catch(e => q.resolve())
}
wrap.clear = wrap.drop
wrap.removeWhere = wrap.remove
wrap.by = (_id) => wrap.find({ _id })
wrap.bulk = function (bulk, cb) {
const batch = collection.initializeUnorderedBulkOp()
try {
bulk.forEach(i => {
if (i.op === 'insert') {
if (i.data._id && typeof i.data._id === 'string' && i.data._id.length === 24) {
i.data._id = new ObjectId(i.data._id)
}
return batch.insert(i.data)
}
const q = { _id: (i.id && i.id.length === 24) ? new ObjectId(i.id + '') : i.id }
if (i.op === 'update') {
return batch.find(q).update(i.update)
}
if (i.op === 'remove') {
return batch.find(q).remove()
}
console.error('UNKNOWN BULK!', i)
})
return q.ninvoke(batch, 'execute')
} catch (e) {
if (cb) cb(e.message)
console.error(e)
return q.reject(e.message)
}
}
return wrap
}
config.common.dbCollections.forEach(i => {
const collection = db.collection(i)
const indexes = config.common.dbIndexes[i]
if (indexes) {
collection.createIndexes(Object.entries(indexes).map(([k, v]) => ({ key: { [k]: v } })))
}
exports.db[i] = wrapCollection(collection, i)
})
return db
})
.then(async (db) => {
const isInitialized = (await db.listCollections().toArray()).find(coll => coll.name === 'users')
// We don't have access to the process type here, but we'll elect 'main' to be the one
// in charge of setting up the entire database if it's not done yet.
if (process.argv[1].endsWith('engine/dist/main.js')) {
if (!isInitialized) {
try {
console.log('Importing database…')
await exports.importDB(path.join(__dirname, '/../../db.original.json'))
await exports.upgradeDB()
} catch (err) {
console.error('An error occured importing existing db, initializing blank server', err)
await importFail()
console.log('Server initialized. Remember to generate rooms.')
}
} else {
console.log('Database already initialized, skipping import')
}
// Then we signal the other process that they're good to go
exports.env.set('DATABASE_READY', true)
return Promise.resolve()
} else {
console.log('Waiting for database…')
return new Promise((resolve) => {
const awaiter = () => {
const val = exports.env.get('DATABASE_READY')
if (val) {
return resolve()
}
setTimeout(awaiter, 100)
}
awaiter()
})
}
})
.then(() => {
console.log('Database ready')
})
Object.assign(exports.pubsub, {
ee: new EventEmitter(),
subscribed: {},
publish (channel, data) {
pub.publish(channel, data)
return q.when()
},
subscribe (channel, cb) {
if (!this.subscribed[channel]) {
if (channel.match(/[?*]/)) { sub.psubscribe(channel) } else { sub.subscribe(channel) }
this.subscribed[channel] = true
}
this.ee.on(channel, (channel, ...args) => {
cb.apply({ channel }, args)
})
return q.when()
},
once (channel, cb) {
if (!this.subscribed[channel]) {
if (channel.match(/[?*]/)) { sub.psubscribe(channel) } else { sub.subscribe(channel) }
this.subscribed[channel] = true
}
this.ee.once(channel, (channel, ...args) => {
cb.apply({ channel }, args)
})
return q.when()
}
})
sub.on('message', (channel, message) => {
exports.pubsub.ee.emit(channel, channel, message)
})
sub.on('pmessage', (pattern, channel, message) => {
exports.pubsub.ee.emit(pattern, channel, message)
})
Object.assign(exports.env, {
get: q.nbind(redis.get, redis),
mget: q.nbind(redis.mget, redis),
sadd: q.nbind(redis.sadd, redis),
smembers: q.nbind(redis.smembers, redis),
set: q.nbind(redis.set, redis),
setex: q.nbind(redis.setex, redis),
expire: q.nbind(redis.expire, redis),
ttl: q.nbind(redis.ttl, redis),
del: q.nbind(redis.del, redis),
hmget: q.nbind(redis.hmget, redis),
hmset: q.nbind(redis.hmset, redis),
hget: q.nbind(redis.hget, redis),
hset: q.nbind(redis.hset, redis),
hgetall: q.nbind(redis.hgetall, redis),
incr: q.nbind(redis.incr, redis),
flushall: q.nbind(redis.flushall, redis)
})
exports._connected = true
exports.resetAllData = () => q.when() // Temp dummy
Object.assign(exports.queue, require('./queue'))
exports.queue.wrap(redis, exports.pubsub)
const oget = exports.env.get
exports.env.get = function (...a) {
return oget(...a).catch(() => exports.env.hgetall(...a))
}
exports.resetAllData = () => {
return exports.importDB(path.join(__dirname, '/../../db.original.json')).then((r) => q.when(r))
}
Object.assign(config.common.storage, exports)
return mongo
}
}
exports.importDB = async function importDB (path = './db.json') {
const { db, env } = exports
const olddb = JSON.parse(fs.readFileSync(path).toString())
const ps = olddb.collections.map(oldcol => {
const name = oldcol.name
console.log('Collection', name)
if (name === 'env') {
return env.flushall().then(() => {
const p = oldcol.data.map(row => {
const ps = []
for (const k in row.data) {
const v = row.data[k]
const type = k.slice(0, k.indexOf(':') + 1)
const hashTypes = [env.keys.MEMORY_SEGMENTS, env.keys.ROOM_HISTORY, env.keys.ROOM_EVENT_LOG]
if (hashTypes.includes(type)) {
for (const kk in v) {
ps.push(env.hmset(k, kk, typeof v[kk] === 'object' ? JSON.stringify(v[kk]) : v[kk]))
}
} else if (k === env.keys.ACTIVE_ROOMS) {
ps.push(env.sadd(env.keys.ACTIVE_ROOMS, v))
} else {
ps.push(env.set(k, typeof v === 'object' ? JSON.stringify(v) : v))
}
}
return Promise.all(ps)
})
return Promise.all(p)
})
} else {
if (!db[name]) {
console.log(`invalid collection in db.json: ${name}`)
return Promise.resolve()
}
return db[name].drop().then(() => Promise.all(oldcol.data.map(row => {
delete row.meta
delete row.$loki
return db[name].insert(row)
})))
}
})
await Promise.all(ps)
await db.users.update({ _id: '2' }, { $set: { _id: '2', username: 'Invader', usernameLower: 'invader', cpu: 100, cpuAvailable: 10000, gcl: 13966610.2, active: 0 } }, { upsert: true })
await db.users.update({ _id: '3' }, { $set: { _id: '3', username: 'Source Keeper', usernameLower: 'source keeper', cpu: 100, cpuAvailable: 10000, gcl: 13966610.2, active: 0 } }, { upsert: true })
await db.users.update({ username: 'Screeps' }, { username: 'Screeps', usernameLower: 'screeps', gcl: 0, cpi: 0, active: false, cpuAvailable: 0, badge: { type: 12, color1: '#999999', color2: '#999999', color3: '#999999', flip: false, param: 26 } }, { upsert: true })
await env.set(env.keys.DATABASE_VERSION, DATABASE_VERSION)
return await upgradeDB()
}
function importFail () {
const { db, env } = exports
return Promise.all([
db.users.insert({ _id: '2', username: 'Invader', usernameLower: 'invader', cpu: 100, cpuAvailable: 10000, gcl: 13966610.2, active: 0 }),
db.users.insert({ _id: '3', username: 'Source Keeper', usernameLower: 'source keeper', cpu: 100, cpuAvailable: 10000, gcl: 13966610.2, active: 0 }),
db.users.insert({ username: 'Screeps', usernameLower: 'screeps', gcl: 0, cpi: 0, active: false, cpuAvailable: 0, badge: { type: 12, color1: '#999999', color2: '#999999', color3: '#999999', flip: false, param: 26 } }),
env.set('gameTime', 1),
env.set(env.keys.DATABASE_VERSION, DATABASE_VERSION)
])
}
async function upgradeDB () {
const { db, env } = exports
const version = parseFloat(await env.get(env.keys.DATABASE_VERSION) || '0')
if (version === DATABASE_VERSION) return
if (version === 0) {
console.log('Database not initialized, skipping upgrade check')
// await exports.resetAllData()
// return upgradeDB()
return
}
console.log(`Database Upgrade needed, current: ${version}, desired: ${DATABASE_VERSION}`)
if (version < 2) {
console.log('Applying version 2')
const ps = []
ps.push(db.users.update({ money: { $gt: 0 } }, { $mul: { money: 1000 } }))
ps.push(db['market.orders'].update({}, { $mul: { price: 1000 } }))
ps.push(db.users.update({ username: 'Screeps' }, { username: 'Screeps', usernameLower: 'screeps', gcl: 0, cpi: 0, active: false, cpuAvailable: 0, badge: { type: 12, color1: '#999999', color2: '#999999', color3: '#999999', flip: false, param: 26 } }, { upsert: true }))
await Promise.all(ps)
}
if (version < 3.1) {
console.log('Applying version 3.1')
await db.rooms.update({}, { $unset: { bus: true } })
await db.rooms.update({ _id: /^[EW]\d*0[NS]\d+$/ }, { $set: { bus: true } })
await db.rooms.update({ _id: /^[EW]\d+[NS]\d*0$/ }, { $set: { bus: true } })
}
if (version < 3.2) {
console.log('Applying version 3.2')
const time = +(await env.get('gameTime'))
await db['rooms.objects'].remove({ type: 'powerCreep', ageTime: { $lt: time } })
}
if (version < 4) { // Factories update
console.log('Applying version 4')
const depositTypes = [C.RESOURCE_SILICON, C.RESOURCE_METAL, C.RESOURCE_BIOMASS, C.RESOURCE_MIST]
const busRooms = await db.rooms.find({ $or: [{ _id: { $regex: /^[WE]\d*0[NS]/ } }, { _id: { $regex: /0$/ } }] })
const ps = []
for (const room of busRooms) {
const [match, longitude, latitude] = /^[WE](\d+)[NS](\d+)$/.exec(room._id)
if (match) {
room.depositType = depositTypes[(longitude + latitude) % 4]
ps.push(db.rooms.update({ _id: room._id }, room))
}
}
await Promise.all(ps)
}
if (version < 5) { // Store update
console.log('Applying version 5')
const ps = []
const energyOnly = function energyOnly (structure) {
structure.store = { energy: structure.energy }
structure.storeCapacityResource = { energy: structure.energyCapacity }
delete structure.energy
delete structure.energyCapacity
}
const storeOnly = function storeOnly (structure) {
if (typeof structure.energyCapacity !== 'undefined') {
structure.storeCapacity = structure.energyCapacity
delete structure.energyCapacity
}
structure.store = {}
C.RESOURCES_ALL.forEach(r => {
if (typeof structure[r] !== 'undefined') {
structure.store[r] = structure[r]
delete structure[r]
}
})
}
const converters = {
spawn: energyOnly,
extension: energyOnly,
tower: energyOnly,
link: energyOnly,
storage: storeOnly,
terminal: storeOnly,
container: storeOnly,
factory: storeOnly,
creep: storeOnly,
powerCreep: storeOnly,
tombstone: storeOnly,
nuker: function nuker (structure) {
structure.store = { energy: structure.energy, G: structure.G }
structure.storeCapacityResource = { energy: structure.energyCapacity, G: structure.GCapacity }
delete structure.energy
delete structure.energyCapacity
delete structure.G
delete structure.GCapacity
},
powerSpawn: function powerSpawn (structure) {
structure.store = { energy: structure.energy, power: structure.power }
structure.storeCapacityResource = { energy: structure.energyCapacity, power: structure.powerCapacity }
delete structure.energy
delete structure.energyCapacity
delete structure.power
delete structure.powerCapacity
},
lab: function lab (structure) {
structure.store = { energy: structure.energy }
structure.storeCapacityResource = { energy: structure.energyCapacity }
if (structure.mineralType && structure.mineralAmount) {
structure.store[structure.mineralType] = structure.mineralAmount
structure.storeCapacityResource[structure.mineralType] = structure.mineralCapacity
} else {
structure.storeCapacity = structure.energyCapacity + structure.mineralCapacity
}
delete structure.energy
delete structure.energyCapacity
delete structure.mineralType
delete structure.mineralAmount
delete structure.mineralCapacity
}
}
const powerCreepsCollection = db['users.power_creeps']
if (powerCreepsCollection) {
const powerCreeps = await powerCreepsCollection.find({})
powerCreeps.forEach(powerCreep => {
console.log(`powerCreep#${powerCreep._id}`)
converters.powerCreep(powerCreep)
const { _id, ...obj } = powerCreep
ps.push(powerCreepsCollection.update({ _id }, obj))
})
}
const roomObjects = await db['rooms.objects'].find({ type: { $in: Object.keys(converters) } })
roomObjects.forEach(object => {
console.log(`${object.type}#${object._id}`)
converters[object.type](object)
const { _id, ...obj } = object
ps.push(db['rooms.objects'].update({ _id }, obj))
})
const nowTimestamp = new Date().getTime()
const orders = await db['market.orders'].find({})
orders.forEach(order => {
if (!order.createdTimestamp) {
console.log(`order#${order._id}`)
order.createdTimestamp = nowTimestamp
const { _id, ...obj } = order
ps.push(db['market.orders'].update({ _id }, obj))
}
})
await Promise.all(ps)
}
if (version < 6) {
console.log('Applying version 6')
const ps = []
const roomObjects = await db['rooms.objects'].find({ type: 'powerBank' })
roomObjects.forEach(object => {
console.log(`${object.type}#${object._id}`)
object.store = { power: object.power }
delete object.power
const { _id, ...obj } = object
ps.push(db['rooms.objects'].update({ _id }, obj))
})
await Promise.all(ps)
}
if (version < 7) {
console.log('Applying version 7')
await db.users.update({ _id: '2' }, {
$set: {
badge: {
type: {
path1: 'm 60.493413,13.745781 -1.122536,7.527255 -23.302365,-6.118884 -24.097204,26.333431 6.412507,0.949878 -5.161481,19.706217 26.301441,24.114728 1.116562,-7.546193 23.350173,6.122868 24.097202,-26.318478 -6.462307,-0.95785 5.16845,-19.699243 z m -1.58271,10.611118 -0.270923,1.821013 C 57.330986,25.69819 55.969864,25.331543 54.570958,25.072546 Z m -8.952409,4.554029 c 11.653612,0 21.055294,9.408134 21.055294,21.069735 0,11.661603 -9.401682,21.068738 -21.055294,21.068738 -11.65361,0 -21.055297,-9.407135 -21.055297,-21.068738 0,-11.661601 9.401687,-21.069735 21.055297,-21.069735 z M 26.634018,40.123069 c -0.262324,0.618965 -0.494865,1.252967 -0.708185,1.895768 l -0.0508,-0.104656 -0.194228,-0.417627 c 0.261245,-0.385697 0.631962,-0.909531 0.953211,-1.373485 z m 47.391601,17.714764 0.115539,0.237219 0.214148,0.462479 c -0.380159,0.55986 -0.886342,1.281124 -1.3835,1.988466 0.400298,-0.870957 0.752837,-1.767746 1.053813,-2.688164 z M 41.364458,73.812322 c 0.694434,0.251619 1.40261,0.471895 2.123558,0.662817 l -2.303841,0.558165 z',
path2: 'm 60.857962,24.035953 -6.397566,1.055531 c 6.084137,1.084905 11.78633,4.394548 15.786244,9.746957 5.741405,7.682749 6.465607,17.544704 2.736121,25.67958 1.511089,-2.147013 2.622575,-3.851337 2.622575,-3.851337 l 1.628526,0.241209 c 0.726895,-2.869027 1.004942,-5.843252 0.811775,-8.806053 l 1.185288,-8.634615 -3.768025,-3.072898 -2.908435,-3.21842 c -0.0103,-0.01383 -0.01958,-0.02805 -0.02988,-0.04186 -3.118009,-4.172293 -7.17889,-7.228662 -11.666624,-9.098091 z M 50.001124,37.965163 A 12.020784,12.029027 0 0 0 37.979913,49.994617 12.020784,12.029027 0 0 0 50.001124,62.024074 12.020784,12.029027 0 0 0 62.022337,49.994617 12.020784,12.029027 0 0 0 50.001124,37.965163 Z M 27.019485,39.55693 c -1.481686,2.114179 -2.5658,3.779575 -2.5658,3.779575 l -1.647451,-0.244197 c -0.69707,2.775045 -0.977606,5.64628 -0.81476,8.511019 l -1.22015,8.890775 3.768021,3.072896 3.422394,3.786551 c 2.921501,3.715734 6.608397,6.499915 10.668588,8.29872 l 5.050921,-1.223973 C 38.324728,73.038607 33.383805,69.887984 29.806406,65.100956 28.655972,63.561522 27.71377,61.932905 26.961715,60.249903 L 24.8272,48.359991 c 0.194234,-3.030146 0.935183,-6.015406 2.192285,-8.803061 z'
},
color1: '#735252',
color2: '#390305',
color3: '#ff0d39',
flip: false
}
}
})
}
if (version < 8) {
console.log('Applying version 8')
const gameTime = parseInt(await env.get(env.keys.GAMETIME))
const roomObjects = await db['rooms.objects'].find({
type: { $in: ['spawn', 'invaderCore'] },
spawning: { $ne: null },
'spawning.remainingTime': { $exists: true }
})
const ps = roomObjects.map(object => {
console.log(`${object.type}#${object._id}: ${JSON.stringify(object.spawning, 0, 2)}`)
object.spawning.spawnTime = gameTime + object.spawning.remainingTime
delete object.spawning.remainingTime
const { _id, ...obj } = object
return db['rooms.objects'].update({ _id }, obj)
})
await Promise.all(ps)
}
if (version < 9) {
console.log('Applying version 9')
const ps = []
const rooms = await db.rooms.find({})
const activeRoomNames = []
rooms.forEach(room => {
if (room.active) {
activeRoomNames.push(room._id)
delete room.active
const { _id, ...obj } = room
ps.push(db.rooms.update({ _id }, obj))
}
})
if (activeRoomNames[0]) {
ps.push(env.sadd(env.keys.ACTIVE_ROOMS, activeRoomNames))
}
await Promise.all(ps)
}
await env.set(env.keys.DATABASE_VERSION, '' + DATABASE_VERSION)
console.log(`Database upgraded to version ${DATABASE_VERSION}`)
}
exports.upgradeDB = upgradeDB