Merge pull request #318 from fantinodavide/eos-integration

EOS integration, major rewrite.
This commit is contained in:
Marek 2024-01-05 15:05:30 -06:00 committed by GitHub
commit dffc8b6119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 561 additions and 160 deletions

View File

@ -16,9 +16,13 @@ export default class LogParser extends EventEmitter {
this.eventStore = {
disconnected: {}, // holding area, cleared on map change.
players: {}, // persistent data, steamid, controller, suffix.
players: [], // persistent data, steamid, controller, suffix.
playersEOS: [], // proxies from EOSID to persistent data, steamid, controller, suffix.
connectionIdToSteamID: new Map(),
session: {}, // old eventstore, nonpersistent data
clients: {} // used in the connection chain before we resolve a player.
clients: {}, // used in the connection chain before we resolve a player.
lastConnection: {}, // used to store the last client connection data to then associate a steamid
joinRequests: []
};
this.linesPerMinute = 0;

View File

@ -337,8 +337,7 @@ export default class Rcon extends EventEmitter {
if (type === SERVERDATA_AUTH) {
this.callbackIds.push({ id: this.count, cmd: body });
Logger.verbose('RCON', 2, `Writing Auth Packet`);
Logger.verbose('RCON', 4, `Writing packet with type "${type}" and body "${body}".`);
this.responseCallbackQueue.push(() => {});
this.responseCallbackQueue.push((decodedPacket) => {
this.client.removeListener('error', onError);
@ -352,7 +351,6 @@ export default class Rcon extends EventEmitter {
}
});
} else {
Logger.verbose('RCON', 2, `Writing packet with type "${type}" and body "${body}".`);
this.callbackIds.push({ id: this.count, cmd: body });
this.responseCallbackQueue.push((response) => {
this.client.removeListener('error', onError);

View File

@ -1,6 +1,6 @@
{
"name": "SquadJS",
"version": "3.8.2",
"version": "4.0.0",
"repository": "https://github.com/Team-Silver-Sphere/SquadJS.git",
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
"license": "BSL-1.0",

View File

@ -1,7 +1,6 @@
import EventEmitter from 'events';
import axios from 'axios';
import Gamedig from 'gamedig';
import Logger from 'core/logger';
import { SQUADJS_API_DOMAIN } from 'core/constants';
@ -19,7 +18,7 @@ export default class SquadServer extends EventEmitter {
constructor(options = {}) {
super();
for (const option of ['host', 'queryPort'])
for (const option of ['host'])
if (!(option in options)) throw new Error(`${option} must be specified.`);
this.id = options.id;
@ -73,13 +72,13 @@ export default class SquadServer extends EventEmitter {
this.admins = await fetchAdminLists(this.options.adminLists);
await this.rcon.connect();
await this.logParser.watch();
await this.updateSquadList();
await this.updatePlayerList();
await this.updatePlayerList(this);
await this.updateLayerInformation();
await this.updateA2SInformation();
await this.logParser.watch();
Logger.verbose('SquadServer', 1, `Watching ${this.serverName}...`);
await this.pingSquadJSAPI();
@ -154,9 +153,12 @@ export default class SquadServer extends EventEmitter {
});
this.rcon.on('SQUAD_CREATED', async (data) => {
data.player = await this.getPlayerBySteamID(data.playerSteamID, true);
data.player = await this.getPlayerByEOSID(data.playerEOSID, true);
data.player.squadID = data.squadID;
delete data.playerName;
delete data.playerSteamID;
delete data.playerEOSID;
this.emit('SQUAD_CREATED', data);
});
@ -207,7 +209,13 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_CONNECTED', async (data) => {
data.player = await this.getPlayerBySteamID(data.steamID);
Logger.verbose(
'SquadServer',
1,
`Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID} - IP: ${data.ip}`
);
data.player = await this.getPlayerByEOSID(data.eosID);
if (data.player) data.player.suffix = data.playerSuffix;
delete data.steamID;
@ -217,7 +225,7 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_DISCONNECTED', async (data) => {
data.player = await this.getPlayerBySteamID(data.steamID);
data.player = await this.getPlayerByEOSID(data.eosID);
delete data.steamID;
@ -226,12 +234,16 @@ export default class SquadServer extends EventEmitter {
this.logParser.on('PLAYER_DAMAGED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
if (data.victim && data.attacker)
if (data.attacker && !data.attacker.playercontroller && data.attackerController)
data.attacker.playercontroller = data.attackerController;
if (data.victim && data.attacker) {
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.steamID !== data.attacker.steamID;
}
delete data.victimName;
delete data.attackerName;
@ -241,7 +253,7 @@ export default class SquadServer extends EventEmitter {
this.logParser.on('PLAYER_WOUNDED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
if (!data.attacker)
data.attacker = await this.getPlayerByController(data.attackerPlayerController);
@ -259,7 +271,7 @@ export default class SquadServer extends EventEmitter {
this.logParser.on('PLAYER_DIED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
if (!data.attacker)
data.attacker = await this.getPlayerByController(data.attackerPlayerController);
@ -275,9 +287,9 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_REVIVED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
data.reviver = await this.getPlayerByName(data.reviverName);
data.victim = await this.getPlayerByEOSID(data.victimEOSID);
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
data.reviver = await this.getPlayerByEOSID(data.reviverEOSID);
delete data.victimName;
delete data.attackerName;
@ -287,7 +299,7 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_POSSESS', async (data) => {
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
data.player = await this.getPlayerByEOSID(data.playerEOSID);
if (data.player) data.player.possessClassname = data.possessClassname;
delete data.playerSuffix;
@ -296,7 +308,7 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_UNPOSSESS', async (data) => {
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
data.player = await this.getPlayerByEOSID(data.playerEOSID);
delete data.playerSuffix;
@ -310,6 +322,23 @@ export default class SquadServer extends EventEmitter {
this.logParser.on('TICK_RATE', (data) => {
this.emit('TICK_RATE', data);
});
this.logParser.on('CLIENT_EXTERNAL_ACCOUNT_INFO', (data) => {
this.rcon.addIds(data.steamID, data.eosID);
});
// this.logParser.on('CLIENT_CONNECTED', (data) => {
// Logger.verbose("SquadServer", 1, `Client connected. Connection: ${data.connection} - SteamID: ${data.steamID}`)
// })
// this.logParser.on('CLIENT_LOGIN_REQUEST', (data) => {
// Logger.verbose("SquadServer", 1, `Login request. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`)
// })
// this.logParser.on('RESOLVED_EOS_ID', (data) => {
// Logger.verbose("SquadServer", 1, `Resolved EOSID. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`)
// })
// this.logParser.on('ADDING_CLIENT_CONNECTION', (data) => {
// Logger.verbose("SquadServer", 1, `Adding client connection`, data)
// })
}
async restartLogParser() {
@ -352,7 +381,7 @@ export default class SquadServer extends EventEmitter {
}
const players = [];
for (const player of await this.rcon.getListPlayers())
for (const player of await this.rcon.getListPlayers(this))
players.push({
...oldPlayerInfo[player.steamID],
...player,
@ -380,6 +409,13 @@ export default class SquadServer extends EventEmitter {
});
}
if (this.a2sPlayerCount > 0 && players.length === 0)
Logger.verbose(
'SquadServer',
1,
`Real Player Count: ${this.a2sPlayerCount} but loaded ${players.length}`
);
this.emit('UPDATED_PLAYER_INFORMATION');
} catch (err) {
Logger.verbose('SquadServer', 1, 'Failed to update player list.', err);
@ -441,53 +477,70 @@ export default class SquadServer extends EventEmitter {
);
}
async updateA2SInformation() {
updateA2SInformation() {
return this.updateServerInformation();
}
async updateServerInformation() {
if (this.updateA2SInformationTimeout) clearTimeout(this.updateA2SInformationTimeout);
Logger.verbose('SquadServer', 1, `Updating A2S information...`);
Logger.verbose('SquadServer', 1, `Updating server information...`);
try {
const data = await Gamedig.query({
type: 'squad',
host: this.options.host,
port: this.options.queryPort
});
const rawData = await this.rcon.execute(`ShowServerInfo`);
Logger.verbose('SquadServer', 3, `Server information raw data`, rawData);
const data = JSON.parse(rawData);
Logger.verbose('SquadServer', 2, `Server information data`, JSON.data);
const info = {
raw: data.raw,
serverName: data.name,
raw: data,
serverName: data.ServerName_s,
maxPlayers: parseInt(data.maxplayers),
publicSlots: parseInt(data.raw.rules.NUMPUBCONN),
reserveSlots: parseInt(data.raw.rules.NUMPRIVCONN),
maxPlayers: parseInt(data.MaxPlayers),
publicQueueLimit: parseInt(data.PublicQueueLimit_I),
reserveSlots: parseInt(data.PlayerReserveCount_I),
a2sPlayerCount: parseInt(data.raw.rules.PlayerCount_i),
publicQueue: parseInt(data.raw.rules.PublicQueue_i),
reserveQueue: parseInt(data.raw.rules.ReservedQueue_i),
playerCount: parseInt(data.PlayerCount_I),
a2sPlayerCount: parseInt(data.PlayerCount_I),
publicQueue: parseInt(data.PublicQueue_I),
reserveQueue: parseInt(data.ReservedQueue_I),
matchTimeout: parseFloat(data.raw.rules.MatchTimeout_f),
gameVersion: data.raw.version
currentLayer: data.MapName_s,
nextLayer: data.NextLayer_s,
teamOne: data.TeamOne_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '',
teamTwo: data.TeamTwo_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '',
matchTimeout: parseFloat(data.MatchTimeout_d),
matchStartTime: this.getMatchStartTimeByPlaytime(data.PLAYTIME_I),
gameVersion: data.GameVersion_s
};
this.serverName = info.serverName;
this.maxPlayers = info.maxPlayers;
this.publicSlots = info.publicSlots;
this.publicSlots = info.maxPlayers - info.reserveSlots;
this.reserveSlots = info.reserveSlots;
this.a2sPlayerCount = info.a2sPlayerCount;
this.a2sPlayerCount = info.playerCount;
this.playerCount = info.playerCount;
this.publicQueue = info.publicQueue;
this.reserveQueue = info.reserveQueue;
this.matchTimeout = info.matchTimeout;
this.matchStartTime = info.matchStartTime;
this.gameVersion = info.gameVersion;
if (!this.currentLayer) this.currentLayer = Layers.getLayerByClassname(info.currentLayer);
if (!this.nextLayer) this.nextLayer = Layers.getLayerByClassname(info.nextLayer);
this.emit('UPDATED_A2S_INFORMATION', info);
this.emit('UPDATED_SERVER_INFORMATION', info);
} catch (err) {
Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err);
Logger.verbose('SquadServer', 1, 'Failed to update server information.', err);
}
Logger.verbose('SquadServer', 1, `Updated A2S information.`);
Logger.verbose('SquadServer', 1, `Updated server information.`);
this.updateA2SInformationTimeout = setTimeout(
this.updateA2SInformation,
@ -542,6 +595,10 @@ export default class SquadServer extends EventEmitter {
return this.getPlayerByCondition((player) => player.steamID === steamID, forceUpdate);
}
async getPlayerByEOSID(eosID, forceUpdate) {
return this.getPlayerByCondition((player) => player.eosID === eosID, forceUpdate);
}
async getPlayerByName(name, forceUpdate) {
return this.getPlayerByCondition((player) => player.name === name, forceUpdate);
}
@ -606,4 +663,8 @@ export default class SquadServer extends EventEmitter {
this.pingSquadJSAPITimeout = setTimeout(this.pingSquadJSAPI, this.pingSquadJSAPIInterval);
}
getMatchStartTimeByPlaytime(playtime) {
return new Date(Date.now() - +playtime * 1000);
}
}

View File

@ -0,0 +1,20 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: AddClientConnection: Added client connection: \[UNetConnection\] RemoteAddr: ([\d.]+):[0-9]+, Name: (EOSIpNetConnection_[0-9]+), Driver: GameNetDriver (EOSNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: INVALID/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
// steamID: args[ 3 ],
ip: args[3],
connection: args[4],
driver: args[5]
};
/* This is Called when unreal engine adds a client connection
First Step in Adding a Player to server
*/
logParser.eventStore['last-connection'] = data;
logParser.emit('ADDING_CLIENT_CONNECTION', data);
}
};

View File

@ -0,0 +1,15 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadCommon: SQCommonStatics Check Permissions, UniqueId:([\da-f]+)$/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: +args[2],
eosID: args[3]
};
logParser.eventStore.joinRequests[data.chainID].eosID = data.eosID;
logParser.emit('RESOLVED_EOS_ID', { ...logParser.eventStore.joinRequests[data.chainID] });
}
};

View File

@ -0,0 +1,21 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]+)]LogEOS: Verbose: \[LogEOSConnect] FConnectClient::CacheExternalAccountInfo - ProductUserId: (?<eosId>[0-9a-f]{32}), AccountType: (\d), AccountId: (?<steamId>[0-9]{17}), DisplayName: <Redacted>/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
eosID: args.groups.eosId,
steamID: args.groups.steamId
};
logParser.eventStore.players[data.steamID] = {
eosID: data.eosID,
steamID: data.steamID
};
logParser.eventStore.playersEOS[data.eosID] = logParser.eventStore.players[data.steamID];
logParser.emit('CLIENT_EXTERNAL_ACCOUNT_INFO', data);
}
};

View File

@ -1,6 +1,6 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: SteamNetConnection \/Engine\/Transient\.(SteamNetConnection_[0-9]+)/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: EOSIpNetConnection \/Engine\/Transient\.(EOSIpNetConnection_[0-9]+)/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
@ -13,7 +13,7 @@ export default {
2nd Step in player connected path
*/
logParser.eventStore['client-login'] = logParser.eventStore.clients[args[3]];
logParser.eventStore.joinRequests[data.chainID].connection = data.connection;
delete logParser.eventStore.clients[args[3]];
logParser.emit('CLIENT_LOGIN', data);
}

View File

@ -16,10 +16,15 @@ import RoundEnded from './round-ended.js';
import RoundTickets from './round-tickets.js';
import RoundWinner from './round-winner.js';
import ServerTickRate from './server-tick-rate.js';
import ClientConnected from './client-connected.js';
import AddingClientConnection from './adding-client-connection.js';
import ClientLogin from './client-login.js';
import PendingConnectionDestroyed from './pending-connection-destroyed.js';
import ClientExternalAccountInfo from './client-external-account-info.js';
import SendingAuthResult from './sending-auth-result.js';
import LoginRequest from './login-request.js';
import JoinRequest from './join-request.js';
import PlayerJoinSucceeded from './player-join-succeeded.js';
import CheckPermissionResolveEosid from './check-permission-resolve-eosid.js';
export default class SquadLogParser extends LogParser {
constructor(options) {
super('SquadGame.log', options);
@ -43,9 +48,15 @@ export default class SquadLogParser extends LogParser {
RoundTickets,
RoundWinner,
ServerTickRate,
ClientConnected,
AddingClientConnection,
ClientLogin,
PendingConnectionDestroyed
PendingConnectionDestroyed,
ClientExternalAccountInfo,
SendingAuthResult,
LoginRequest,
JoinRequest,
PlayerJoinSucceeded,
CheckPermissionResolveEosid
];
}
}

View File

@ -0,0 +1,15 @@
export default {
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join request: .+\?Name=(.+)\?SplitscreenCount=\d$/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: +args[2],
suffix: args[3]
};
logParser.eventStore.joinRequests[data.chainID] = data;
// console.log(logParser.eventStore.loginRequests[ data.chainID ])
logParser.emit('CLIENT_JOIN_REQUEST', data);
}
};

View File

@ -0,0 +1,17 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+?)(?:\?PASSWORD=(?:.+?))? userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: +args[2],
suffix: args[3],
eosID: args[4]
};
// logParser.eventStore.loginRequests[ data.chainID ] = data;
// console.log(logParser.eventStore.loginRequests[ data.chainID ])
logParser.emit('CLIENT_LOGIN_REQUEST', data);
}
};

View File

@ -1,6 +1,6 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9a-f]{32}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],

View File

@ -1,27 +1,25 @@
export default {
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/,
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: PostLogin: NewPlayer: BP_PlayerController_C .+PersistentLevel\.([^\s]+) \(IP: ([\d.]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d+)\)/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
playerSuffix: args[3],
steamID: logParser.eventStore['client-login'], // player connected
controller: logParser.eventStore['player-controller'] // playercontroller connected
chainID: +args[2],
playercontroller: args[3],
ip: args[4],
eosID: args[5],
steamID: args[6]
};
delete logParser.eventStore['client-login'];
delete logParser.eventStore['player-controller'];
const joinRequestData = logParser.eventStore.joinRequests[+args[2]];
data.connection = joinRequestData.connection;
data.playerSuffix = joinRequestData.suffix;
if (!logParser.eventStore.players[data.steamID])
logParser.eventStore.players[data.steamID] = {};
logParser.eventStore.players[data.steamID].controller = data.playercontroller;
// Handle Reconnecting players
if (logParser.eventStore.disconnected[data.steamID]) {
delete logParser.eventStore.disconnected[data.steamID];
}
logParser.emit('PLAYER_CONNECTED', data);
logParser.eventStore.players[data.steamID] = {
steamID: data.steamID,
suffix: data.playerSuffix,
controller: data.controller
};
}
};

View File

@ -1,6 +1,6 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) caused by ([A-z_0-9-]+)_C/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Player Controller ID: ([^ ]+)\)caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
@ -9,11 +9,18 @@ export default {
victimName: args[3],
damage: parseFloat(args[4]),
attackerName: args[5],
weapon: args[6]
attackerEOSID: args[6],
attackerSteamID: args[7],
attackerController: args[8],
weapon: args[9]
};
logParser.eventStore.session[args[3]] = data;
if (!logParser.eventStore.players[data.attackerSteamID])
logParser.eventStore.players[data.attackerSteamID] = {};
logParser.eventStore.players[data.attackerSteamID].controller = data.attackerController;
logParser.emit('PLAYER_DAMAGED', data);
}
};

View File

@ -1,6 +1,6 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Contoller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
const data = {
...logParser.eventStore.session[args[3]],
@ -11,7 +11,9 @@ export default {
victimName: args[3],
damage: parseFloat(args[4]),
attackerPlayerController: args[5],
weapon: args[6]
attackerEOSID: args[6],
attackerSteamID: args[7],
weapon: args[9]
};
logParser.eventStore.session[args[3]] = data;

View File

@ -1,16 +1,18 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: SteamNetConnection_[0-9]+, Driver: GameNetDriver SteamNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([\d.]+):[\d]+, Name: EOSIpNetConnection_[0-9]+, Driver: GameNetDriver EOSNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+, UniqueId: RedpointEOS:([\d\w]+)/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
steamID: args[3],
playerController: args[4]
ip: args[3],
playerController: args[4],
eosID: args[5]
};
logParser.eventStore.disconnected[data.steamID] = true;
logParser.emit('PLAYER_DISCONNECTED', data);
}
};

View File

@ -0,0 +1,28 @@
export default {
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: +args[2],
playerSuffix: args[3]
};
const joinRequestsData = { ...logParser.eventStore.joinRequests[data.chainID] };
data.eosID = joinRequestsData.eosID;
data.controller = joinRequestsData.controller;
data.steamID = `${logParser.eventStore.connectionIdToSteamID.get(joinRequestsData.connection)}`;
logParser.eventStore.connectionIdToSteamID.delete(joinRequestsData.connection);
delete logParser.eventStore.joinRequests[+data.chainID];
// Handle Reconnecting players
if (logParser.eventStore.disconnected[data.steamID]) {
delete logParser.eventStore.disconnected[data.steamID];
}
logParser.emit('JOIN_SUCCEEDED', data);
}
};

View File

@ -1,13 +1,15 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=([A-z0-9_]+)_C/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\) Pawn=([A-z0-9_]+)_C/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
playerSuffix: args[3],
possessClassname: args[4],
playerEOSID: args[4],
playerSteamID: args[5],
possessClassname: args[6],
pawn: args[5]
};

View File

@ -1,6 +1,7 @@
export default {
// the names are currently the wrong way around in these logs
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) has revived (.+)\./,
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has revived (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\)\./,
onMatch: (args, logParser) => {
const data = {
...logParser.eventStore.session[args[3]],
@ -8,7 +9,11 @@ export default {
time: args[1],
chainID: args[2],
reviverName: args[3],
victimName: args[4]
reviverEOSID: args[4],
reviverSteamID: args[5],
victimName: args[6],
victimEOSID: args[7],
victimSteamID: args[8]
};
logParser.emit('PLAYER_REVIVED', data);

View File

@ -1,14 +1,16 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+)/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\)/,
onMatch: (args, logParser) => {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
playerSuffix: args[3],
playerEOSID: args[4],
playerSteamID: args[5],
switchPossess:
args[3] in logParser.eventStore.session && logParser.eventStore.session[args[3]] === args[2]
args[4] in logParser.eventStore.session && logParser.eventStore.session[args[4]] === args[2]
};
delete logParser.eventStore.session[args[3]];

View File

@ -1,6 +1,6 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/,
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Controller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
onMatch: (args, logParser) => {
const data = {
...logParser.eventStore.session[args[3]],
@ -10,7 +10,9 @@ export default {
victimName: args[3],
damage: parseFloat(args[4]),
attackerPlayerController: args[5],
weapon: args[6]
attackerEOSID: args[6],
attackerSteamID: args[7],
weapon: args[9]
};
logParser.eventStore.session[args[3]] = data;

View File

@ -5,12 +5,11 @@ export default {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
chainID: +args[2],
controller: args[3]
};
logParser.eventStore['player-controller'] = args[3];
logParser.eventStore.joinRequests[data.chainID].controller = data.controller;
logParser.emit('PLAYER_CONTROLLER_CONNECTED', data);
}
};

View File

@ -0,0 +1,21 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogOnline: STEAM: AUTH HANDLER: Sending auth result to user (\d{17}) with flag success\? 1/,
onMatch: (args, logParser) => {
if (!logParser.eventStore['last-connection']) return;
const data = {
...logParser.eventStore['last-connection'],
steamID: args[3]
};
/* This is Called when unreal engine adds a client connection
First Step in Adding a Player to server
*/
logParser.eventStore.clients[data.connection] = data.steamID;
logParser.eventStore.connectionIdToSteamID.set(data.connection, data.steamID);
logParser.emit('CLIENT_CONNECTED', data);
delete logParser.eventStore['last-connection'];
}
};

View File

@ -2,7 +2,7 @@ import Sequelize from 'sequelize';
import BasePlugin from './base-plugin.js';
const { DataTypes } = Sequelize;
const { DataTypes, QueryTypes } = Sequelize;
export default class DBLog extends BasePlugin {
static get description() {
@ -146,6 +146,44 @@ export default class DBLog extends BasePlugin {
}
);
this.createModel(
'Player',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
eosID: {
type: DataTypes.STRING,
unique: true
},
steamID: {
type: DataTypes.STRING,
notNull: true,
unique: true
},
lastName: {
type: DataTypes.STRING
},
lastIP: {
type: DataTypes.STRING
}
},
{
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
indexes: [
{
fields: ['eosID']
},
{
fields: ['steamID']
}
]
}
);
this.createModel(
'Wound',
{
@ -329,37 +367,44 @@ export default class DBLog extends BasePlugin {
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Wound, {
this.models.Player.hasMany(this.models.Wound, {
sourceKey: 'steamID',
foreignKey: { name: 'attacker' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Wound, {
this.models.Player.hasMany(this.models.Wound, {
sourceKey: 'steamID',
foreignKey: { name: 'victim' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Death, {
this.models.Player.hasMany(this.models.Death, {
sourceKey: 'steamID',
foreignKey: { name: 'attacker' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Death, {
this.models.Player.hasMany(this.models.Death, {
sourceKey: 'steamID',
foreignKey: { name: 'victim' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Revive, {
this.models.Player.hasMany(this.models.Revive, {
sourceKey: 'steamID',
foreignKey: { name: 'attacker' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Revive, {
this.models.Player.hasMany(this.models.Revive, {
sourceKey: 'steamID',
foreignKey: { name: 'victim' },
onDelete: 'CASCADE'
});
this.models.SteamUser.hasMany(this.models.Revive, {
this.models.Player.hasMany(this.models.Revive, {
sourceKey: 'steamID',
foreignKey: { name: 'reviver' },
onDelete: 'CASCADE'
});
@ -392,9 +437,12 @@ export default class DBLog extends BasePlugin {
this.onTickRate = this.onTickRate.bind(this);
this.onUpdatedA2SInformation = this.onUpdatedA2SInformation.bind(this);
this.onNewGame = this.onNewGame.bind(this);
this.onPlayerConnected = this.onPlayerConnected.bind(this);
this.onPlayerWounded = this.onPlayerWounded.bind(this);
this.onPlayerDied = this.onPlayerDied.bind(this);
this.onPlayerRevived = this.onPlayerRevived.bind(this);
this.migrateSteamUsersIntoPlayers = this.migrateSteamUsersIntoPlayers.bind(this);
this.dropAllForeignKeys = this.dropAllForeignKeys.bind(this);
}
createModel(name, schema) {
@ -409,12 +457,15 @@ export default class DBLog extends BasePlugin {
await this.models.TickRate.sync();
await this.models.PlayerCount.sync();
await this.models.SteamUser.sync();
await this.models.Player.sync();
await this.models.Wound.sync();
await this.models.Death.sync();
await this.models.Revive.sync();
}
async mount() {
await this.migrateSteamUsersIntoPlayers();
await this.models.Server.upsert({
id: this.options.overrideServerID || this.server.id,
name: this.server.serverName
@ -427,6 +478,7 @@ export default class DBLog extends BasePlugin {
this.server.on('TICK_RATE', this.onTickRate);
this.server.on('UPDATED_A2S_INFORMATION', this.onUpdatedA2SInformation);
this.server.on('NEW_GAME', this.onNewGame);
this.server.on('PLAYER_CONNECTED', this.onPlayerConnected);
this.server.on('PLAYER_WOUNDED', this.onPlayerWounded);
this.server.on('PLAYER_DIED', this.onPlayerDied);
this.server.on('PLAYER_REVIVED', this.onPlayerRevived);
@ -436,6 +488,7 @@ export default class DBLog extends BasePlugin {
this.server.removeEventListener('TICK_RATE', this.onTickRate);
this.server.removeEventListener('UPDATED_A2S_INFORMATION', this.onTickRate);
this.server.removeEventListener('NEW_GAME', this.onNewGame);
this.server.removeEventListener('PLAYER_CONNECTED', this.onPlayerConnected);
this.server.removeEventListener('PLAYER_WOUNDED', this.onPlayerWounded);
this.server.removeEventListener('PLAYER_DIED', this.onPlayerDied);
this.server.removeEventListener('PLAYER_REVIVED', this.onPlayerRevived);
@ -479,15 +532,27 @@ export default class DBLog extends BasePlugin {
async onPlayerWounded(info) {
if (info.attacker)
await this.models.SteamUser.upsert({
steamID: info.attacker.steamID,
lastName: info.attacker.name
});
await this.models.Player.upsert(
{
eosID: info.attacker.eosID,
steamID: info.attacker.steamID,
lastName: info.attacker.name
},
{
conflictFields: ['steamID']
}
);
if (info.victim)
await this.models.SteamUser.upsert({
steamID: info.victim.steamID,
lastName: info.victim.name
});
await this.models.Player.upsert(
{
eosID: info.victim.eosID,
steamID: info.victim.steamID,
lastName: info.victim.name
},
{
conflictFields: ['steamID']
}
);
await this.models.Wound.create({
server: this.options.overrideServerID || this.server.id,
@ -509,15 +574,27 @@ export default class DBLog extends BasePlugin {
async onPlayerDied(info) {
if (info.attacker)
await this.models.SteamUser.upsert({
steamID: info.attacker.steamID,
lastName: info.attacker.name
});
await this.models.Player.upsert(
{
eosID: info.attacker.eosID,
steamID: info.attacker.steamID,
lastName: info.attacker.name
},
{
conflictFields: ['steamID']
}
);
if (info.victim)
await this.models.SteamUser.upsert({
steamID: info.victim.steamID,
lastName: info.victim.name
});
await this.models.Player.upsert(
{
eosID: info.victim.eosID,
steamID: info.victim.steamID,
lastName: info.victim.name
},
{
conflictFields: ['steamID']
}
);
await this.models.Death.create({
server: this.options.overrideServerID || this.server.id,
@ -540,20 +617,38 @@ export default class DBLog extends BasePlugin {
async onPlayerRevived(info) {
if (info.attacker)
await this.models.SteamUser.upsert({
steamID: info.attacker.steamID,
lastName: info.attacker.name
});
await this.models.Player.upsert(
{
eosID: info.attacker.eosID,
steamID: info.attacker.steamID,
lastName: info.attacker.name
},
{
conflictFields: ['steamID']
}
);
if (info.victim)
await this.models.SteamUser.upsert({
steamID: info.victim.steamID,
lastName: info.victim.name
});
await this.models.Player.upsert(
{
eosID: info.victim.eosID,
steamID: info.victim.steamID,
lastName: info.victim.name
},
{
conflictFields: ['steamID']
}
);
if (info.reviver)
await this.models.SteamUser.upsert({
steamID: info.reviver.steamID,
lastName: info.reviver.name
});
await this.models.Player.upsert(
{
eosID: info.reviver.eosID,
steamID: info.reviver.steamID,
lastName: info.reviver.name
},
{
conflictFields: ['steamID']
}
);
await this.models.Revive.create({
server: this.options.overrideServerID || this.server.id,
@ -577,4 +672,89 @@ export default class DBLog extends BasePlugin {
reviverSquadID: info.reviver ? info.reviver.squadID : null
});
}
async onPlayerConnected(info) {
await this.models.Player.upsert(
{
eosID: info.eosID,
steamID: info.player.steamID,
lastName: info.player.name,
lastIP: info.ip
},
{
conflictFields: ['steamID']
}
);
}
async migrateSteamUsersIntoPlayers() {
try {
const steamUsersCount = await this.models.SteamUser.count();
const playersCount = await this.models.Player.count();
if (steamUsersCount < playersCount) {
this.verbose(
1,
`Skipping migration from SteamUsers to Players due to a previous successful migration.`
);
return;
}
await this.dropAllForeignKeys();
const steamUsers = (await this.models.SteamUser.findAll()).map((u) => u.dataValues);
await this.models.Player.bulkCreate(steamUsers);
this.verbose(1, `Migration from SteamUsers to Players successful`);
} catch (error) {
this.verbose(1, `Error during Migration from SteamUsers to Players: ${error}`);
}
}
async dropAllForeignKeys() {
this.verbose(
1,
`Starting to drop constraints on DB: ${this.options.database.config.database} related to DBLog_SteamUsers deptecated table.`
);
for (const modelName in this.models) {
const model = this.models[modelName];
const tableName = model.tableName;
try {
const result = await this.options.database.query(
`SELECT * FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL AND table_schema = '${this.options.database.config.database}' AND table_name = '${tableName}';`,
{ type: QueryTypes.SELECT }
);
for (const r of result) {
if (r.REFERENCED_TABLE_NAME === 'DBLog_SteamUsers') {
this.verbose(
1,
`Found constraint ${r.COLUMN_NAME} on table ${tableName}, referencing ${r.REFERENCED_COLUMN_NAME} on ${r.REFERENCED_TABLE_NAME}`
);
await this.options.database
.query(`ALTER TABLE ${tableName} DROP FOREIGN KEY ${r.CONSTRAINT_NAME}`, {
type: QueryTypes.RAW
})
.then(() => {
this.verbose(1, `Dropped foreign key ${r.COLUMN_NAME} on table ${tableName}`);
})
.catch((e) => {
this.verbose(
1,
`Error dropping foreign key ${r.COLUMN_NAME} on table ${tableName}:`,
e
);
});
}
}
} catch (error) {
this.verbose(1, `Error dropping foreign keys for table ${tableName}:`, error);
} finally {
model.sync();
}
}
await this.models.Player.sync();
}
}

View File

@ -4,7 +4,7 @@ import Rcon from 'core/rcon';
export default class SquadRcon extends Rcon {
processChatPacket(decodedPacket) {
const matchChat = decodedPacket.body.match(
/\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9]{17})] (.+?) : (.*)/
/\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+?) : (.*)/
);
if (matchChat) {
Logger.verbose('SquadRcon', 2, `Matched chat message: ${decodedPacket.body}`);
@ -12,9 +12,10 @@ export default class SquadRcon extends Rcon {
this.emit('CHAT_MESSAGE', {
raw: decodedPacket.body,
chat: matchChat[1],
steamID: matchChat[2],
name: matchChat[3],
message: matchChat[4],
eosID: matchChat[2],
steamID: matchChat[3],
name: matchChat[4],
message: matchChat[5],
time: new Date()
});
@ -22,14 +23,14 @@ export default class SquadRcon extends Rcon {
}
const matchPossessedAdminCam = decodedPacket.body.match(
/\[SteamID:([0-9]{17})] (.+?) has possessed admin camera./
/\[Online Ids:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has possessed admin camera\./
);
if (matchPossessedAdminCam) {
Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`);
this.emit('POSSESSED_ADMIN_CAMERA', {
raw: decodedPacket.body,
steamID: matchPossessedAdminCam[1],
name: matchPossessedAdminCam[2],
steamID: matchPossessedAdminCam[2],
name: matchPossessedAdminCam[3],
time: new Date()
});
@ -37,14 +38,14 @@ export default class SquadRcon extends Rcon {
}
const matchUnpossessedAdminCam = decodedPacket.body.match(
/\[SteamID:([0-9]{17})] (.+?) has unpossessed admin camera./
/\[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has unpossessed admin camera\./
);
if (matchUnpossessedAdminCam) {
Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`);
this.emit('UNPOSSESSED_ADMIN_CAMERA', {
raw: decodedPacket.body,
steamID: matchUnpossessedAdminCam[1],
name: matchUnpossessedAdminCam[2],
steamID: matchUnpossessedAdminCam[2],
name: matchUnpossessedAdminCam[3],
time: new Date()
});
@ -68,7 +69,7 @@ export default class SquadRcon extends Rcon {
}
const matchKick = decodedPacket.body.match(
/Kicked player ([0-9]+)\. \[steamid=([0-9]{17})] (.*)/
/Kicked player ([0-9]+)\. \[Online IDs= EOS: ([0-9a-f]{32}) steam: (\d{17})] (.*)/
);
if (matchKick) {
Logger.verbose('SquadRcon', 2, `Matched kick message: ${decodedPacket.body}`);
@ -76,8 +77,8 @@ export default class SquadRcon extends Rcon {
this.emit('PLAYER_KICKED', {
raw: decodedPacket.body,
playerID: matchKick[1],
steamID: matchKick[2],
name: matchKick[3],
steamID: matchKick[3],
name: matchKick[4],
time: new Date()
});
@ -85,18 +86,14 @@ export default class SquadRcon extends Rcon {
}
const matchSqCreated = decodedPacket.body.match(
/(.+) \(Steam ID: ([0-9]{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/
/(?<playerName>.+) \(Online IDs: EOS: (?<playerEOSID>[\da-f]{32})(?: steam: (?<playerSteamID>\d{17}))?\) has created Squad (?<squadID>\d+) \(Squad Name: (?<squadName>.+)\) on (?<teamName>.+)/
);
if (matchSqCreated) {
Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`);
this.emit('SQUAD_CREATED', {
time: new Date(),
playerName: matchSqCreated[1],
playerSteamID: matchSqCreated[2],
squadID: matchSqCreated[3],
squadName: matchSqCreated[4],
teamName: matchSqCreated[5]
...matchSqCreated.groups
});
return;
@ -143,19 +140,17 @@ export default class SquadRcon extends Rcon {
for (const line of response.split('\n')) {
const match = line.match(
/ID: ([0-9]+) \| SteamID: ([0-9]{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/
/^ID: (?<playerID>\d+) \| Online IDs: EOS: (?<eosID>[a-f\d]{32}) (?:steam: (?<steamID>\d{17}) )?\| Name: (?<name>.+) \| Team ID: (?<teamID>\d|N\/A) \| Squad ID: (?<squadID>\d+|N\/A) \| Is Leader: (?<isLeader>True|False) \| Role: (?<role>.+)$/
);
if (!match) continue;
players.push({
playerID: match[1],
steamID: match[2],
name: match[3],
teamID: match[4],
squadID: match[5] !== 'N/A' ? match[5] : null,
isLeader: match[6] === 'True',
role: match[7]
});
const data = match.groups;
data.playerID = +data.playerID;
data.isLeader = data.isLeader === 'True';
data.teamID = data.teamID !== 'N/A' ? +data.teamID : null;
data.squadID = data.squadID !== 'N/A' ? +data.squadID : null;
players.push(data);
}
return players;
@ -172,21 +167,17 @@ export default class SquadRcon extends Rcon {
for (const line of responseSquad.split('\n')) {
const match = line.match(
/ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Steam ID: ([0-9]{17})/
/ID: (?<squadID>\d+) \| Name: (?<squadName>.+) \| Size: (?<size>\d+) \| Locked: (?<locked>True|False) \| Creator Name: (?<creatorName>.+) \| Creator Online IDs: EOS: (?<creatorEOSID>[a-f\d]{32})(?: steam: (?<creatorSteamID>\d{17}))?/
);
const matchSide = line.match(/Team ID: (1|2) \((.+)\)/);
const matchSide = line.match(/Team ID: (\d) \((.+)\)/);
if (matchSide) {
teamID = matchSide[1];
teamID = +matchSide[1];
teamName = matchSide[2];
}
if (!match) continue;
match.groups.squadID = +match.groups.squadID;
squads.push({
squadID: match[1],
squadName: match[2],
size: match[3],
locked: match[4],
creatorName: match[5],
creatorSteamID: match[6],
...match.groups,
teamID: teamID,
teamName: teamName
});