refactor player data storage completely
This commit is contained in:
parent
69966ec746
commit
f82cbb736c
10 changed files with 297 additions and 63 deletions
|
@ -35,7 +35,7 @@ module.exports = {
|
|||
node.permissionLevel = 0
|
||||
},
|
||||
|
||||
sendCommand (context) {
|
||||
async sendCommand (context) {
|
||||
const source = context.source
|
||||
const bot = source.bot
|
||||
const player = source.getPlayerOrThrow()
|
||||
|
@ -43,7 +43,13 @@ module.exports = {
|
|||
const username = context.getArgument('username')
|
||||
const message = context.getArgument('message')
|
||||
|
||||
bot.sendMail(player.username, username, message)
|
||||
try {
|
||||
await bot.sendMail(player.username, username, message)
|
||||
} catch (error) {
|
||||
source.sendError('Unable to send mail: ' + error)
|
||||
return
|
||||
}
|
||||
|
||||
bot.tellraw([
|
||||
{ text: 'Sent ', ...bot.styles.primary },
|
||||
{ text: message, ...bot.styles.secondary },
|
||||
|
@ -56,16 +62,17 @@ module.exports = {
|
|||
const source = context.source
|
||||
const bot = source.bot
|
||||
const player = source.getPlayerOrThrow()
|
||||
const playerData = bot.playerData[player.uuid]
|
||||
|
||||
const messages = bot.mail[player.username]
|
||||
if (!messages || messages.length < 1) {
|
||||
const messages = playerData.data?.mail
|
||||
if (!messages || !messages.length) {
|
||||
bot.tellraw({ text: 'You have no mail', ...bot.styles.primary }, createUuidSelector(player.uuid))
|
||||
return
|
||||
}
|
||||
|
||||
const msg = [{ text: 'Mail:\n', ...bot.styles.primary }]
|
||||
messages.forEach((message) => {
|
||||
msg.push(`${message.sender} (from ${message.host}): `)
|
||||
msg.push(`${message.sender} (from ${message.host}:${message.port}): `)
|
||||
msg.push({ text: `${message.message}\n`, ...bot.styles.secondary })
|
||||
})
|
||||
msg[msg.length - 1].text = msg[msg.length - 1].text.slice(0, -1)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const { literal, argument, string, DynamicCommandExceptionType } = require('brigadier-commands')
|
||||
const TextMessage = require('../util/command/text_message')
|
||||
|
||||
const NEVER_SEEN_ERROR = new DynamicCommandExceptionType(username => new TextMessage([username, ' was never seen']))
|
||||
|
||||
module.exports = {
|
||||
|
@ -16,14 +17,26 @@ module.exports = {
|
|||
node.permissionLevel = 0
|
||||
},
|
||||
|
||||
seenCommand (context) {
|
||||
async seenCommand (context) {
|
||||
const source = context.source
|
||||
const bot = source.bot
|
||||
|
||||
const username = context.getArgument('username')
|
||||
if (bot.seen[username] == null) throw NEVER_SEEN_ERROR.create(username)
|
||||
|
||||
const { first, last } = bot.seen[username]
|
||||
let playerData = Object.values(bot.playerData).find(playerData => playerData.data.username === username)
|
||||
|
||||
if (!playerData) {
|
||||
try {
|
||||
playerData = await bot.loadPlayerData(username)
|
||||
} catch (error) {
|
||||
source.sendError('Unable to load player data: ' + error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!playerData.data?.seen) throw NEVER_SEEN_ERROR.create()
|
||||
|
||||
const { first, last } = playerData.data.seen
|
||||
source.sendFeedback([
|
||||
{ text: '', ...bot.styles.primary },
|
||||
{ text: username, ...bot.styles.secondary },
|
||||
|
|
|
@ -1,27 +1,19 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const filepath = path.resolve('persistent', 'mail.json')
|
||||
|
||||
// load the blacklist
|
||||
let mail = {}
|
||||
try {
|
||||
mail = require(filepath)
|
||||
} catch (e) {
|
||||
console.log('An error occured while loading the mail.')
|
||||
}
|
||||
|
||||
// save it every 5 minutes
|
||||
setInterval(() => {
|
||||
fs.writeFileSync(filepath, JSON.stringify(mail))
|
||||
}, 5 * 6000)
|
||||
|
||||
// make the bot uuid ban blacklisted players
|
||||
function inject (bot) {
|
||||
bot.mail = mail
|
||||
bot.sendMail = (sender, reciever, message) => {
|
||||
if (!mail[reciever]) mail[reciever] = []
|
||||
mail[reciever].push({ sender: sender, message, host: bot.host })
|
||||
bot.sendMail = sendMail
|
||||
|
||||
async function sendMail (sender, receiver, message) {
|
||||
let playerData = Object.values(bot.playerData).find(playerData => playerData.data.username === receiver)
|
||||
|
||||
let loadedManually = false
|
||||
if (!playerData) {
|
||||
playerData = await bot.loadPlayerData(receiver)
|
||||
loadedManually = true
|
||||
}
|
||||
|
||||
playerData.data.mail ??= []
|
||||
playerData.data.mail.push({ sender: sender, message, host: bot.host, port: bot.port })
|
||||
|
||||
if (loadedManually) await playerData.save()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ function inject (bot, options) {
|
|||
}
|
||||
|
||||
bot.matrix.client.on('Room.timeline', (event, room, toStartOfTimeline) => {
|
||||
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.getTs() < startTime || event.sender.userId === bot.matrix.client.getUserId()) return
|
||||
if (event.getRoomId() !== bot.matrix.roomId || event.getType() !== 'm.room.message' || event.getTs() < startTime || event.sender.userId === bot.matrix.client.getUserId() || !bot.loggedIn) return
|
||||
|
||||
const content = event.getContent()
|
||||
const permissionLevel = event.sender.powerLevelNorm
|
||||
|
|
92
plugins/player_data.js
Normal file
92
plugins/player_data.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
const fs = require('fs/promises')
|
||||
const path = require('path')
|
||||
const nbt = require('prismarine-nbt')
|
||||
const PlayerData = require('../util/player_data')
|
||||
const fileExists = require('../util/file_exists')
|
||||
const getOfflineUUID = require('../util/offline_player_uuid')
|
||||
|
||||
const playerData = {}
|
||||
|
||||
async function inject (bot) {
|
||||
const persistentDir = bot.paths.persistent
|
||||
const playerDataDir = path.join(persistentDir, 'playerdata')
|
||||
|
||||
if (!await fileExists(playerDataDir)) await fs.mkdir(playerDataDir)
|
||||
|
||||
bot.playerData = playerData
|
||||
bot.loadPlayerData = loadPlayerData
|
||||
|
||||
if (bot.loggedIn && bot.players) {
|
||||
for (const player of bot.players) {
|
||||
// If we logged in while the async file operations were happening (highly unlikely but possible), load the player data for the players on the server
|
||||
handlePlayerAdded(player)
|
||||
}
|
||||
}
|
||||
|
||||
bot.on('player_added', handlePlayerAdded)
|
||||
|
||||
|
||||
async function handlePlayerAdded (player) {
|
||||
if (player.uuid === bot.uuid) return
|
||||
|
||||
let data = playerData[player.uuid]
|
||||
|
||||
if (data) {
|
||||
data._bots.add(bot)
|
||||
return
|
||||
}
|
||||
|
||||
data = await loadPlayerData(player.username)
|
||||
data._bots.add(bot)
|
||||
|
||||
playerData[player.uuid] = data
|
||||
data.data.username = player.username
|
||||
bot.emit('player_data_loaded', player, data)
|
||||
}
|
||||
|
||||
async function loadPlayerData (uuid) {
|
||||
if (uuid.length <= 16) {
|
||||
// Usernames
|
||||
uuid = getOfflineUUID(uuid)
|
||||
}
|
||||
|
||||
const data = new PlayerData(path.join(playerDataDir, uuid + '.dat'))
|
||||
await data.load()
|
||||
return data
|
||||
}
|
||||
|
||||
bot.on('player_removed', async player => {
|
||||
const data = playerData[player.uuid]
|
||||
if (!data) return
|
||||
|
||||
data._bots.delete(bot)
|
||||
if (data._bots.size) return
|
||||
|
||||
bot.emit('player_data_unloading', player, data)
|
||||
|
||||
await data.unload(true)
|
||||
delete bot.playerData[player.uuid]
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAll () {
|
||||
console.log('Saving player data...')
|
||||
for (const uuid in playerData) {
|
||||
const data = playerData[uuid]
|
||||
if (!data?.data) {
|
||||
// We _somehow_ found some unloaded data
|
||||
delete playerData[uuid]
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await data.save()
|
||||
} catch (error) {
|
||||
console.error('Unable to write data for player %s:', data?.data?.username, error)
|
||||
}
|
||||
}
|
||||
console.log('Saved!')
|
||||
}
|
||||
setInterval(saveAll, 60 * 3 * 1000)
|
||||
|
||||
module.exports = inject
|
|
@ -1,42 +1,22 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const filepath = path.resolve('persistent', 'seen.json')
|
||||
|
||||
// load the seen data
|
||||
let seen = {}
|
||||
try {
|
||||
seen = require(filepath)
|
||||
} catch (e) {
|
||||
console.log('An error occured while loading seen players.')
|
||||
}
|
||||
|
||||
// save it every 5 minutes
|
||||
setInterval(() => {
|
||||
fs.writeFileSync(filepath, JSON.stringify(seen))
|
||||
}, 5 * 6000)
|
||||
|
||||
// expose the data to the bot
|
||||
function inject (bot) {
|
||||
bot.seen = seen
|
||||
bot.on('player_data_loaded', (player, data) => {
|
||||
data.data.seen ??= {}
|
||||
const seenData = data.data.seen
|
||||
|
||||
bot.on('player_added', player => {
|
||||
if (player.uuid === bot.uuid) return
|
||||
|
||||
seen[player.username] ??= {}
|
||||
if (seen[player.username].first == null) {
|
||||
seen[player.username].first = new Date()
|
||||
bot.core.run('minecraft:tellraw @a ' + JSON.stringify([
|
||||
if (seenData.first == null) {
|
||||
seenData.first = new Date()
|
||||
bot.tellraw([
|
||||
{ text: 'Welcome ', ...bot.styles.primary },
|
||||
{ text: player.username, ...bot.styles.secondary },
|
||||
' to the server!'
|
||||
]))
|
||||
], '@a')
|
||||
}
|
||||
seen[player.username].last = new Date()
|
||||
seenData.last = new Date()
|
||||
})
|
||||
|
||||
bot.on('player_removed', player => {
|
||||
if (seen[player.username] != null) seen[player.username].last = new Date()
|
||||
bot.on('player_data_unloading', (player, data) => {
|
||||
const seenData = data.data.seen
|
||||
if (seenData != null) seenData.last = new Date()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
BIN
test.nbt
Normal file
BIN
test.nbt
Normal file
Binary file not shown.
19
util/offline_player_uuid.js
Normal file
19
util/offline_player_uuid.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const crypto = require('crypto')
|
||||
|
||||
function getOfflineUUID (username) {
|
||||
// Hash the UUID
|
||||
const md5 = crypto.createHash('md5')
|
||||
md5.update('OfflinePlayer:' + username, 'utf-8')
|
||||
const hash = md5.digest()
|
||||
|
||||
// From https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/UUID.java#L182-L185
|
||||
hash[6] &= 0x0f /* clear version */
|
||||
hash[6] |= 0x30 /* set to version 3 */
|
||||
hash[8] &= 0x3f /* clear variant */
|
||||
hash[8] |= 0x80 /* set to IETF variant */
|
||||
|
||||
const hex = hash.toString('hex')
|
||||
return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}`
|
||||
}
|
||||
|
||||
module.exports = getOfflineUUID
|
60
util/persistent_data.js
Normal file
60
util/persistent_data.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const fs = require('fs/promises')
|
||||
const nbt = require('prismarine-nbt')
|
||||
const fileExists = require('./file_exists')
|
||||
|
||||
class PersistentData {
|
||||
#endianness
|
||||
data = null
|
||||
|
||||
constructor (filepath) {
|
||||
this.filepath = filepath
|
||||
this.loaded = false
|
||||
}
|
||||
|
||||
async load () {
|
||||
const { parsed, type } = await this.#loadData(this.filepath, true)
|
||||
|
||||
this.data = this.parse(parsed)
|
||||
this.#endianness = type
|
||||
this.loaded = true
|
||||
}
|
||||
|
||||
async #loadData (filepath, tryBackup) {
|
||||
if (!await fileExists(filepath)) {
|
||||
return { parsed: nbt.comp({}), type: 'big' }
|
||||
}
|
||||
|
||||
const buffer = await fs.readFile(filepath)
|
||||
|
||||
try {
|
||||
return nbt.parse(buffer)
|
||||
} catch (error) {
|
||||
if (tryBackup) return this.#loadData(filepath + '_old', false)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async save () {
|
||||
const data = this.unparse(this.data)
|
||||
if (await fileExists(this.filepath)) await fs.copyFile(this.filepath, this.filepath + '_old') // Back up our data before a save
|
||||
await fs.writeFile(this.filepath, nbt.writeUncompressed(data, this.#endianness))
|
||||
}
|
||||
|
||||
async unload (save) {
|
||||
if (save) await this.save()
|
||||
|
||||
this.data = null
|
||||
this.#endianness = undefined
|
||||
this.loaded = false
|
||||
}
|
||||
|
||||
parse (data) {
|
||||
return data
|
||||
}
|
||||
|
||||
unparse (data) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PersistentData
|
71
util/player_data.js
Normal file
71
util/player_data.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
const PersistentData = require('./persistent_data')
|
||||
const nbt = require('prismarine-nbt')
|
||||
|
||||
class PlayerData extends PersistentData {
|
||||
_bots = new Set()
|
||||
|
||||
parse (data) {
|
||||
const parsed = {}
|
||||
data = data.value // * Convenient since I don't need to write data.value a bunch of times
|
||||
|
||||
if (data.username) parsed.username = data.username?.value
|
||||
|
||||
if (data.seen?.value) {
|
||||
parsed.seen = {}
|
||||
if (data.seen.value.first?.value) parsed.seen.first = new Date(Number(data.seen.value.first?.value))
|
||||
if (data.seen.value.last?.value) parsed.seen.last = new Date(Number(data.seen.value.last?.value))
|
||||
}
|
||||
|
||||
if (data.mail?.value?.value && Array.isArray(data.mail.value.value)) {
|
||||
parsed.mail = data.mail.value.value.map(this.#parseMail)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
#parseMail (mail) {
|
||||
const signedPort = Number(mail?.port?.value ?? 25565)
|
||||
const uint16Array = new Uint16Array(1)
|
||||
uint16Array[0] = signedPort
|
||||
const unsignedPort = uint16Array[0]
|
||||
|
||||
return {
|
||||
sender: nbt.comp({ uuid: mail?.sender?.uuid }),
|
||||
message: String(mail?.message?.value),
|
||||
host: String(mail?.host?.value),
|
||||
port: unsignedPort
|
||||
}
|
||||
}
|
||||
|
||||
unparse (parsed) {
|
||||
const data = {}
|
||||
|
||||
if (parsed.username) data.username = nbt.string(parsed.username)
|
||||
|
||||
if (parsed.seen) {
|
||||
data.seen = nbt.comp({ first: nbt.long(BigInt(parsed.seen.first.getTime())), last: nbt.long(BigInt(parsed.seen.last.getTime())) })
|
||||
}
|
||||
|
||||
if (parsed.mail) {
|
||||
data.mail = nbt.list(nbt.comp(parsed.mail.map(this.#unparseMail)))
|
||||
}
|
||||
|
||||
return nbt.comp(data)
|
||||
}
|
||||
|
||||
#unparseMail (mail) {
|
||||
const unsignedPort = mail.port ?? 25565
|
||||
const int16Array = new Int16Array(1)
|
||||
int16Array[0] = unsignedPort
|
||||
const signedPort = int16Array[0]
|
||||
|
||||
return {
|
||||
sender: nbt.string(mail.sender),
|
||||
message: nbt.string(mail.message),
|
||||
host: nbt.string(mail.host),
|
||||
port: nbt.short(signedPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayerData
|
Loading…
Reference in a new issue