import { Inject, Injectable } from "../../Injector/ServiceDecorator"; import { RaidManagerIfc, RaidManagerFeatureIfc } from "./RPCInterface"; import { FrontworkComponent } from "../../Types/FrontworkComponent"; import { TableDefiniton, Signup, Raid, Character, RaidData, Spec, SRToken, Item, User } from "../../Types/Types"; import { IAdmin } from "../../Admin/Interface"; import { IRaidManager } from "./Interface"; import { IUserManager } from "../User/Interface"; import { ICharacterManager } from "../Character/Interface"; import { _Tiers } from "../../Types/Items"; import { MAX_MEMO_LENGTH } from "../../Types/Constants"; import { IItemManager } from "../Item/Interface"; import { ItemManager } from "../Item/ItemManager"; import { IPubSub } from "../PubSub/Interface"; import { getLogger } from "log4js"; @Injectable(IRaidManager) export class RaidManager implements FrontworkComponent, IRaidManager { name = "RaidManager" as "RaidManager"; @Inject(IAdmin) private admin: IAdmin @Inject(IUserManager) private userManager: IUserManager @Inject(ICharacterManager) private characterManager: ICharacterManager @Inject(ItemManager) private itemManager: IItemManager @Inject(IPubSub) private pubsub: IPubSub RPCs = () => [ this.getRaids, this.getRaidData, this.getPastRaids, this.getArchiveRaid ] RPCFeatures() { return [{ name: 'manageRaid' as 'manageRaid', RPCs: () => [ this.createRaid, this.addSignup, this.removeSignup, this.archiveRaid, this.updateSignup, this.startRaid, this.adminUnsign, this.cancelRaid ] }, { name: 'signup' as 'signup', RPCs: () => [ this.getSignups, this.sign, this.unsign ] }] } getTableDefinitions(): TableDefiniton[] { return [ { name: 'raids', tableBuilder: (table) => { table.increments('id').primary() table.dateTime('start').notNullable() table.string('description').notNullable() table.string('title').notNullable() table.integer('size').defaultTo(40) table.string('tier').defaultTo('null') } }, { name: 'archive', tableBuilder: (table) => { table.integer('id').primary() table.json('raiddata').notNullable() } }, { name: 'signups', tableBuilder: (table) => { table.increments('id').primary() table.unique(['raidid', 'characterid']) table.integer('raidid') table.foreign('raidid').references('id').inTable('raids').onDelete('CASCADE') table.integer('characterid') table.foreign('characterid').references('id').inTable('characters').onDelete('CASCADE') table.boolean('benched').defaultTo('false') table.boolean('late') table.timestamp('timestamp').defaultTo(this.admin.knex.fn.now()) table.string('memo').nullable() } } ] } notifyRaid = async (raid: Raid | { id: number }) => { const data = await this.getRaidData(raid) try { await this.pubsub.publish(String(raid.id), data) await this.notifyRaids() } catch (e) { getLogger('RaidManager#notifyRaid').debug(e); } } notifyRaids = async () => { try { await this.pubsub.publish('raids', undefined) } catch (e) { getLogger('RaidManager#notifyRaids').debug(e); } } createRaid = async (raid: Raid): Promise => { const ids: number[] = await this.admin .knex('raids') .insert(raid) await this.notifyRaid({ id: ids[0] }) return await this.admin.knex('raids').where({ id: ids[0] }).first() } addSignup = async (signup: Signup) => { const ids: number[] = await this.admin .knex('signups') .insert(signup) return await this.admin.knex('signups').where({ id: ids[0] }).first() } removeSignup = async (signup: Signup) => await this.admin .knex('signups') .where({ raid_id: signup.raidid, character_id: signup.characterid }) .del() getRaids = async (): Promise => { const countSignups = this.admin .knex('signups') .count('*') .where({ raidid: this.admin.knex.ref('raids.id'), benched: false, late: false }) .as('signupcount') const countBenches = this.admin .knex('signups') .count('*') .where({ raidid: this.admin.knex.ref('raids.id'), benched: true, late: false }) .as('benchcount') return await this.admin.knex('raids') .select('*', countSignups, countBenches) .orderBy('start', 'asc') } startRaid = async (raid: Raid): Promise => { const data = await this.getRaidData(raid) data.participants.absent.map(p => this.itemManager.decayTokensOfCharacter(raid.tier, p, 1)) await this.itemManager.decayTokens(data.tier) const archived = await this.archiveRaid(raid) const giveCurrency = async (b: Character) => { const usr = await this.characterManager.getUserOfCharacter(b) await this.userManager.incrementCurrency(usr, raid.tier, 1) } await Promise.all([ ...archived.participants.bench.map(giveCurrency), ...Object.values(archived.participants).flat().map((b: Signup & Character & Spec) => giveCurrency(b)) ]) await this.notifyRaids() return archived } archiveRaid = async (raid: Raid): Promise => { const raidData = await this.getRaidData(raid) const tx = await this.admin.knex.transaction() await this.admin.knex('archive') .transacting(tx) .insert({ id: raidData.id, raiddata: JSON.stringify(raidData) }) await Promise.all( Object.values(raidData.participants).flat().flatMap((p: (Signup & Character & Spec)) => this.admin .knex(raid.tier + 'tokens') .transacting(tx) .where({ characterid: p.characterid, signupid: null }) .del() )) await this.admin.knex('raids') .transacting(tx) .where('id', '=', raid.id) .del() await tx.commit() const row = await this.admin.knex('archive') .select('*') .where({ id: raidData.id, }) .first() try { return JSON.parse(row.raiddata) } catch (e) { getLogger('RaidManager#archiveRaid').error(row.id + " could not get parsed") return {} as RaidData } } getArchiveRaid = async (id: number): Promise => { const data = await this.admin .knex('archive') .select('raiddata') .where({ id: id }) .first() try { return JSON.parse(data.raiddata) } catch (e) { getLogger('RaidManager#getArchiveRaid').error(id + " could not get parsed") return {} as RaidData } } getPastRaids = async (limit: number): Promise => { const raids = await this.admin.knex('archive') .select('*') .orderBy('id', 'desc') .limit(limit) return raids.map(raid => JSON.parse(raid.raiddata)) } getRaidData = async (raid: Raid): Promise => { const raiddata = { participants: { Druid: <(Signup & Character & Spec)[]>[], Hunter: <(Signup & Character & Spec)[]>[], Mage: <(Signup & Character & Spec)[]>[], Paladin: <(Signup & Character & Spec)[]>[], Priest: <(Signup & Character & Spec)[]>[], Rogue: <(Signup & Character & Spec)[]>[], Shaman: <(Signup & Character & Spec)[]>[], Warlock: <(Signup & Character & Spec)[]>[], Warrior: <(Signup & Character & Spec)[]>[], late: <(Signup & Character & Spec)[]>[], bench: <(Signup & Character & Spec)[]>[], absent: <(Signup & Character & Spec)[]>[] }, tokens: {}, healers: <(Signup & Character & Spec)[]>[], tanks: <(Signup & Character & Spec)[]>[] } const subQuery = this.admin .knex('signups') .count('*') .where({ raidid: this.admin.knex.ref('raids.id'), benched: false, late: false, absent: false }) .as('signupcount') const raidInDb: Raid = await this.admin.knex('raids') .select('*', subQuery) .where('id', '=', raid.id) .first() const characterData: (Signup & Character & Spec)[] = await this.admin .knex('signups as s') .select('s.id as id', 'charactername', 'rank', 'class', 'specid', 'specname', 'race', 'userid', 'benched', 'late', 'raidid', 'characterid', 'specid', 'memo', 'timestamp', 'absent') .join('raids as r', 's.raidid', '=', 'r.id') .join('characters as c', 's.characterid', '=', 'c.id') .join('users as u', 'c.userid', '=', 'u.id') .join('specs as sp', 'specid', '=', 'sp.id') .where('r.id', '=', raid.id) characterData.forEach(data => { if(data.absent){ raiddata.participants.absent.push(data) return } if (data.benched) { raiddata.participants.bench.push(data) return } if (data.late) { raiddata.participants.late.push(data) return } raiddata.participants[data.class].push(data) }) const tokenData: (Character & SRToken & Item)[] = await this.admin .knex('signups as s') .select('*', 's.id as id') .join('raids as r', 's.raidid', '=', 'r.id') .join('characters as c', 's.characterid', '=', 'c.id') .join('users as u', 'u.id', '=', 'c.userid') .join(raidInDb.tier + 'tokens as t', 't.characterid', '=', 'c.id') .join('items as i', 'i.itemname', '=', 't.itemname') .where({ 'r.id': raid.id, }) .andWhere(function () { //this.whereNotIn('u.rank', ['Trial', 'Guest']) this.whereNotNull('t.signupid') }) tokenData.forEach(data => { if (!raiddata.tokens[data.itemname]) raiddata.tokens[data.itemname] = [] raiddata.tokens[data.itemname].push(data) }) raiddata.tanks = Object.values(raiddata.participants).flatMap( (tanks: any[]) => tanks.filter((p: any) => !p.benched && !p.absent && !p.late && (p.specname === "Protection" || p.specname === "Feral (Tank)")) ) raiddata.healers = Object.values(raiddata.participants).flatMap( (healers: any[]) => healers.filter((p: any) => !p.benched && !p.absent && !p.late && (p.specname === "Holy" || p.specname === "Discipline" || p.specname === "Restoration")) ) return { ...raidInDb, ...raiddata } } getSignups = async (raid: Raid): Promise<(Signup & Character & Spec & User)[]> => await this.admin .knex('signups as si') .join('characters as c', 'c.id', '=', 'characterid') .join('specs as s', 's.id', '=', 'specid') .join('users as u', 'u.id', '=', 'userid') .select('*', 'si.id as id') .where('raidid', '=', raid.id!) sign = async (usertoken: string, character: Character, raid: Raid, late: boolean, absent:boolean, memo?: string) => { const maybeUserRecord = this.userManager.getUserRecordByToken(usertoken) if (!maybeUserRecord || maybeUserRecord.user.id != character.userid) { throw new Error("Bad Usertoken") } if (memo) memo = memo.substring(0, MAX_MEMO_LENGTH) const exists = await this.admin .knex('signups') .select('*') .where({ raidid: raid.id!, characterid: character.id!, }) .first() if (!exists) { await this.admin .knex('signups') .insert({ raidid: raid.id!, characterid: character.id!, late: late, absent: absent, benched: false, memo: memo }) } else { await this.admin .knex('signups') .where({ id: exists.id }) .update({ raidid: raid.id!, characterid: character.id!, late: late, absent: absent, benched: false, memo: memo }) } await this.notifyRaid(raid) return await this.admin .knex('signups') .select('*') .where({ raidid: raid.id!, characterid: character.id!, }) .first() } cancelRaid = async (raid: Raid) => { const data = await this.getRaidData(raid) const participants = Object.values(data.participants).flat() await Promise.all( participants.map(async p => await this.adminUnsign({...p, id: p.characterid}, raid)) ) await this.admin.knex('raids').where({id: raid.id}).del() await this.notifyRaids() } unsign = async (usertoken: string, character: Character, raid: Raid) => { const maybeUserRecord = this.userManager.getUserRecordByToken(usertoken) if (!maybeUserRecord || maybeUserRecord.user.id != character.userid) { throw new Error("Bad Usertoken") } return await this.adminUnsign(character, raid) } adminUnsign = async (character: Character, raid: Raid) => { const user = await this.characterManager.getUserOfCharacter(character) const tokens = await this.itemManager.getTokens(character, [raid.tier], true) //check if token has to be deleted if (tokens) { await Promise.all( tokens.map(async token => { await this.userManager.incrementCurrency(user, raid.tier, 1) const prio = await this.itemManager.calculatePriorities(token.itemname, character) if (token.level <= prio + 2) { await this.admin .knex(raid.tier + 'tokens') .where({ characterid: character.id, itemname: token.itemname }).del() } else { await this.admin .knex(raid.tier + 'tokens') .where({ characterid: character.id, itemname: token.itemname }).update({ signupid: null, level: token.level - 2 }) } }) ) } await this.admin.knex('signups') .where({ raidid: raid.id!, characterid: character.id!, }) .del() await this.notifyRaid(raid) } updateSignup = async (signup: Signup): Promise => { await this.admin.knex('signups') .where({ raidid: signup.raidid, characterid: signup.characterid }) .update(signup) await this.notifyRaid({ id: signup.raidid }) } }