@demostf/demo.js
Advanced tools
Comparing version 2.1.0 to 2.1.1
@@ -84,2 +84,5 @@ import { handleGameEvent } from '../PacketHandler/GameEvent'; | ||
const user = this.users.get(userId); | ||
if (user && user.userId !== userId) { | ||
throw new Error(`Invalid user info id`); | ||
} | ||
if (!user) { | ||
@@ -86,0 +89,0 @@ const entityInfo = this.parserState.getUserEntityInfo(userId); |
@@ -5,2 +5,3 @@ import { BitStream } from 'bit-buffer'; | ||
import { EntityId, PacketEntity } from './PacketEntity'; | ||
import { Game } from './ParserState'; | ||
import { SendTable } from './SendTable'; | ||
@@ -154,3 +155,3 @@ import { ServerClass } from './ServerClass'; | ||
platform: string; | ||
game: string; | ||
game: Game; | ||
skybox: string; | ||
@@ -157,0 +158,0 @@ serverName: string; |
@@ -43,4 +43,6 @@ import { SendProp } from './SendProp'; | ||
} | ||
result.serialNumber = this.serialNumber; | ||
if (this.delay) { | ||
if (this.serialNumber) { | ||
result.serialNumber = this.serialNumber; | ||
} | ||
if (typeof this.delay !== 'undefined') { | ||
result.delay = this.delay; | ||
@@ -47,0 +49,0 @@ } |
@@ -12,2 +12,3 @@ import { BitStream } from 'bit-buffer'; | ||
import { UserEntityInfo, UserId } from './UserInfo'; | ||
export declare type Game = 'tf' | 'hl2mp'; | ||
export declare class ParserState { | ||
@@ -27,2 +28,3 @@ version: number; | ||
tick: number; | ||
game: Game; | ||
handlePacket(packet: Packet): void; | ||
@@ -29,0 +31,0 @@ handleMessage(message: Message): void; |
@@ -28,2 +28,3 @@ import { handleGameEventList } from '../PacketHandler/GameEventList'; | ||
this.version = packet.version; | ||
this.game = packet.game; | ||
break; | ||
@@ -65,3 +66,3 @@ case 'stringTable': | ||
getUserEntityInfo(userId) { | ||
const info = this.userInfo.get(userId); | ||
const info = this.userInfo.get(JSON.parse(JSON.stringify(userId))); | ||
if (info) { | ||
@@ -68,0 +69,0 @@ return info; |
import { PVS } from '../Data/PacketEntity'; | ||
import { Player } from '../Data/Player'; | ||
import { Vector } from '../Data/Vector'; | ||
import { handleBaseEntity } from './BaseEntityHandler'; | ||
import { handleHL2DMEntity } from './HL2DMEntityHandler'; | ||
import { handleTFEntity } from './TFEntityHandler'; | ||
export function handlePacketEntities(packet, match, message) { | ||
for (const entity of packet.entities) { | ||
handleEntity(entity, match, message); | ||
handleBaseEntity(entity, match, message); | ||
switch (match.parserState.game) { | ||
case 'tf': | ||
handleTFEntity(entity, match, message); | ||
break; | ||
case 'hl2mp': | ||
handleHL2DMEntity(entity, match, message); | ||
break; | ||
} | ||
} | ||
@@ -23,463 +32,2 @@ } | ||
} | ||
function handleEntity(entity, match, message) { | ||
for (const prop of entity.props) { | ||
if (prop.definition.ownerTableName === 'DT_AttributeContainer' && prop.definition.name === 'm_hOuter') { | ||
if (!match.outerMap.has(prop.value)) { | ||
match.outerMap.set(prop.value, entity.entityIndex); | ||
} | ||
} | ||
} | ||
for (const prop of entity.props) { | ||
if (prop.definition.ownerTableName === 'DT_BaseCombatWeapon' && prop.definition.name === 'm_hOwner') { | ||
if (!match.weaponMap.has(entity.entityIndex)) { | ||
match.weaponMap.set(entity.entityIndex, { | ||
className: entity.serverClass.name, | ||
owner: prop.value | ||
}); | ||
} | ||
} | ||
} | ||
switch (entity.serverClass.name) { | ||
case 'CWorld': | ||
match.world.boundaryMin = entity.getProperty('DT_WORLD', 'm_WorldMins').value; | ||
match.world.boundaryMax = entity.getProperty('DT_WORLD', 'm_WorldMaxs').value; | ||
break; | ||
case 'CTFPlayer': | ||
/** | ||
* "DT_TFPlayerScoringDataExclusive.m_iCaptures": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iDefenses": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iKills": 5, | ||
* "DT_TFPlayerScoringDataExclusive.m_iDeaths": 17, | ||
* "DT_TFPlayerScoringDataExclusive.m_iSuicides": 7, | ||
* "DT_TFPlayerScoringDataExclusive.m_iDominations": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iRevenge": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iBuildingsBuilt": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iBuildingsDestroyed": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iHeadshots": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iBackstabs": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iHealPoints": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iInvulns": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iTeleports": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iDamageDone": 847, | ||
* "DT_TFPlayerScoringDataExclusive.m_iCrits": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iResupplyPoints": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iKillAssists": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iBonusPoints": 0, | ||
* "DT_TFPlayerScoringDataExclusive.m_iPoints": 6, | ||
* "DT_TFPlayerSharedLocal.m_nDesiredDisguiseTeam": 0, | ||
* "DT_TFPlayerSharedLocal.m_nDesiredDisguiseClass": 0, | ||
* "DT_TFPlayerShared.m_iKillStreak": 0, | ||
* "DT_TFPlayerShared.m_flCloakMeter": 100, | ||
*/ | ||
const userInfo = match.getUserInfoForEntity(entity); | ||
if (!userInfo) { | ||
throw new Error(`No user info for entity ${entity.entityIndex}`); | ||
} | ||
const player = (match.playerEntityMap.has(entity.entityIndex)) ? | ||
match.playerEntityMap.get(entity.entityIndex) : | ||
new Player(match, userInfo); | ||
if (!match.playerEntityMap.has(entity.entityIndex)) { | ||
match.playerEntityMap.set(entity.entityIndex, player); | ||
} | ||
for (const prop of entity.props) { | ||
if (prop.definition.ownerTableName === 'm_hMyWeapons') { | ||
if (prop.value !== 2097151) { | ||
player.weaponIds[parseInt(prop.definition.name, 10)] = prop.value; | ||
} | ||
} | ||
if (prop.definition.ownerTableName === 'm_iAmmo') { | ||
if (prop.value !== null && prop.value > 0) { | ||
player.ammo[parseInt(prop.definition.name, 10)] = prop.value; | ||
} | ||
} | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
switch (propName) { | ||
case 'DT_BasePlayer.m_iHealth': | ||
player.health = prop.value; | ||
break; | ||
case 'DT_BasePlayer.m_iMaxHealth': | ||
player.maxHealth = prop.value; | ||
break; | ||
case 'DT_TFLocalPlayerExclusive.m_vecOrigin': | ||
player.position.x = prop.value.x; | ||
player.position.y = prop.value.y; | ||
// set the view angles for the local player since that prop isn't send | ||
player.viewAngle = message.localViewAngles[0].y; | ||
break; | ||
case 'DT_TFNonLocalPlayerExclusive.m_vecOrigin': | ||
player.position.x = prop.value.x; | ||
player.position.y = prop.value.y; | ||
break; | ||
case 'DT_TFLocalPlayerExclusive.m_vecOrigin[2]': | ||
player.position.z = prop.value; | ||
break; | ||
case 'DT_TFNonLocalPlayerExclusive.m_vecOrigin[2]': | ||
player.position.z = prop.value; | ||
break; | ||
case 'DT_TFNonLocalPlayerExclusive.m_angEyeAngles[1]': | ||
player.viewAngle = prop.value; | ||
break; | ||
case 'DT_TFLocalPlayerExclusive.m_angEyeAngles[1]': | ||
player.viewAngle = prop.value; | ||
break; | ||
case 'DT_BasePlayer.m_lifeState': | ||
player.lifeState = prop.value; | ||
break; | ||
case 'DT_BaseCombatCharacter.m_hActiveWeapon': | ||
for (let i = 0; i < player.weapons.length; i++) { | ||
if (player.weaponIds[i] === prop.value) { | ||
player.activeWeapon = i; | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
case 'CWeaponMedigun': | ||
const weapon = match.weaponMap.get(entity.entityIndex); | ||
if (weapon && weapon.className === 'CWeaponMedigun') { | ||
for (const prop of entity.props) { | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
switch (propName) { | ||
case 'DT_WeaponMedigun.m_hHealingTarget': | ||
weapon.healTarget = prop.value; | ||
break; | ||
case 'DT_TFWeaponMedigunDataNonLocal.m_flChargeLevel': | ||
weapon.chargeLevel = prop.value; | ||
break; | ||
case 'DT_LocalTFWeaponMedigunData.m_flChargeLevel': | ||
weapon.chargeLevel = prop.value; | ||
break; | ||
} | ||
} | ||
} | ||
break; | ||
case 'CTFTeam': | ||
if (entity.hasProperty('DT_Team', 'm_iTeamNum')) { | ||
const teamId = entity.getProperty('DT_Team', 'm_iTeamNum').value; | ||
if (!match.teams.has(teamId)) { | ||
const team = { | ||
name: entity.getProperty('DT_Team', 'm_szTeamname').value, | ||
score: entity.getProperty('DT_Team', 'm_iScore').value, | ||
roundsWon: entity.getProperty('DT_Team', 'm_iRoundsWon').value, | ||
players: entity.getProperty('DT_Team', '"player_array"').value, | ||
teamNumber: teamId | ||
}; | ||
match.teams.set(teamId, team); | ||
match.teamEntityMap.set(entity.entityIndex, team); | ||
} | ||
} | ||
else { | ||
const team = match.teamEntityMap.get(entity.entityIndex); | ||
if (!team) { | ||
throw new Error(`No team with entity id: ${entity.entityIndex}`); | ||
} | ||
for (const prop of entity.props) { | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
switch (propName) { | ||
case 'DT_Team.m_iScore': | ||
team.score = prop.value; | ||
break; | ||
case 'DT_Team.m_szTeamname': | ||
team.name = prop.value; | ||
break; | ||
case 'DT_Team.m_iRoundsWon': | ||
team.roundsWon = prop.value; | ||
break; | ||
case 'DT_Team."player_array"': | ||
team.players = prop.value; | ||
break; | ||
} | ||
} | ||
} | ||
break; | ||
case 'CObjectSentrygun': | ||
if (!match.buildings.has(entity.entityIndex)) { | ||
match.buildings.set(entity.entityIndex, { | ||
type: 'sentry', | ||
ammoRockets: 0, | ||
ammoShells: 0, | ||
autoAimTarget: 0, | ||
builder: 0, | ||
health: 0, | ||
isBuilding: false, | ||
isSapped: false, | ||
level: 0, | ||
maxHealth: 0, | ||
playerControlled: false, | ||
position: new Vector(0, 0, 0), | ||
shieldLevel: 0, | ||
isMini: false, | ||
team: 0, | ||
angle: 0 | ||
}); | ||
} | ||
const sentry = match.buildings.get(entity.entityIndex); | ||
for (const prop of entity.props) { | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
applyBuildingProp(sentry, prop, propName); | ||
switch (propName) { | ||
case 'DT_ObjectSentrygun.m_bPlayerControlled': | ||
sentry.playerControlled = prop.value > 0; | ||
break; | ||
case 'DT_ObjectSentrygun.m_hAutoAimTarget': | ||
sentry.autoAimTarget = prop.value; | ||
break; | ||
case 'DT_ObjectSentrygun.m_nShieldLevel': | ||
sentry.shieldLevel = prop.value; | ||
break; | ||
case 'DT_ObjectSentrygun.m_iAmmoShells': | ||
sentry.ammoShells = prop.value; | ||
break; | ||
case 'DT_ObjectSentrygun.m_iAmmoRockets': | ||
sentry.ammoRockets = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_bMiniBuilding': | ||
sentry.isMini = prop.value > 1; | ||
break; | ||
case 'DT_TFNonLocalPlayerExclusive.m_angEyeAngles[1]': | ||
sentry.angle = prop.value; | ||
break; | ||
} | ||
} | ||
if (entity.pvs & PVS.LEAVE) { | ||
match.buildings.delete(entity.entityIndex); | ||
} | ||
break; | ||
case 'CObjectDispenser': | ||
if (!match.buildings.has(entity.entityIndex)) { | ||
match.buildings.set(entity.entityIndex, { | ||
type: 'dispenser', | ||
builder: 0, | ||
health: 0, | ||
isBuilding: false, | ||
isSapped: false, | ||
level: 0, | ||
maxHealth: 0, | ||
position: new Vector(0, 0, 0), | ||
team: 0, | ||
healing: [], | ||
metal: 0, | ||
angle: 0 | ||
}); | ||
} | ||
const dispenser = match.buildings.get(entity.entityIndex); | ||
for (const prop of entity.props) { | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
applyBuildingProp(dispenser, prop, propName); | ||
switch (propName) { | ||
case 'DT_ObjectDispenser.m_iAmmoMetal': | ||
dispenser.metal = prop.value; | ||
break; | ||
case 'DT_ObjectDispenser."healing_array"': | ||
dispenser.healing = prop.value; | ||
break; | ||
} | ||
} | ||
if (entity.pvs & PVS.LEAVE) { | ||
match.buildings.delete(entity.entityIndex); | ||
} | ||
break; | ||
case 'CObjectTeleporter': | ||
if (!match.buildings.has(entity.entityIndex)) { | ||
match.buildings.set(entity.entityIndex, { | ||
type: 'teleporter', | ||
builder: 0, | ||
health: 0, | ||
isBuilding: false, | ||
isSapped: false, | ||
level: 0, | ||
maxHealth: 0, | ||
position: new Vector(0, 0, 0), | ||
team: 0, | ||
isEntrance: false, | ||
otherEnd: 0, | ||
rechargeTime: 0, | ||
rechargeDuration: 0, | ||
timesUsed: 0, | ||
angle: 0, | ||
yawToExit: 0 | ||
}); | ||
} | ||
const teleporter = match.buildings.get(entity.entityIndex); | ||
for (const prop of entity.props) { | ||
const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
applyBuildingProp(teleporter, prop, propName); | ||
switch (propName) { | ||
case 'DT_ObjectTeleporter.m_flRechargeTime': | ||
teleporter.rechargeTime = prop.value; | ||
break; | ||
case 'DT_ObjectTeleporter.m_flCurrentRechargeDuration': | ||
teleporter.rechargeDuration = prop.value; | ||
break; | ||
case 'DT_ObjectTeleporter.m_iTimesUsed': | ||
teleporter.timesUsed = prop.value; | ||
break; | ||
case 'DT_ObjectTeleporter.m_bMatchBuilding': | ||
teleporter.otherEnd = prop.value; | ||
break; | ||
case 'DT_ObjectTeleporter.m_flYawToExit': | ||
teleporter.yawToExit = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_iObjectMode': | ||
teleporter.isEntrance = prop.value === 0; | ||
break; | ||
} | ||
} | ||
if (entity.pvs & PVS.LEAVE) { | ||
match.buildings.delete(entity.entityIndex); | ||
} | ||
break; | ||
case 'CTFPlayerResource': | ||
for (const prop of entity.props) { | ||
const playerId = parseInt(prop.definition.name, 10); | ||
const value = prop.value; | ||
if (!match.playerResources[playerId]) { | ||
match.playerResources[playerId] = { | ||
alive: false, | ||
arenaSpectator: false, | ||
bonusPoints: 0, | ||
chargeLevel: 0, | ||
connected: false, | ||
damageAssists: 0, | ||
damageBlocked: 0, | ||
deaths: 0, | ||
dominations: 0, | ||
healing: 0, | ||
healingAssist: 0, | ||
health: 0, | ||
killStreak: 0, | ||
maxBuffedHealth: 0, | ||
maxHealth: 0, | ||
nextRespawn: 0, | ||
ping: 0, | ||
playerClass: 0, | ||
playerLevel: 0, | ||
score: 0, | ||
team: 0, | ||
totalScore: 0, | ||
damage: 0 | ||
}; | ||
} | ||
const playerResource = match.playerResources[playerId]; | ||
switch (prop.definition.ownerTableName) { | ||
case 'm_iPing': | ||
playerResource.ping = value; | ||
break; | ||
case 'm_iScore': | ||
playerResource.score = value; | ||
break; | ||
case 'm_iDeaths': | ||
playerResource.deaths = value; | ||
break; | ||
case 'm_bConnected': | ||
playerResource.connected = value > 0; | ||
break; | ||
case 'm_iTeam': | ||
playerResource.team = value; | ||
break; | ||
case 'm_bAlive': | ||
playerResource.alive = value > 0; | ||
break; | ||
case 'm_iHealth': | ||
playerResource.health = value; | ||
break; | ||
case 'm_iTotalScore': | ||
playerResource.totalScore = value; | ||
break; | ||
case 'm_iMaxHealth': | ||
playerResource.maxHealth = value; | ||
break; | ||
case 'm_iMaxBuffedHealth': | ||
playerResource.maxBuffedHealth = value; | ||
break; | ||
case 'm_iPlayerClass': | ||
playerResource.playerClass = value; | ||
break; | ||
case 'm_bArenaSpectator': | ||
playerResource.arenaSpectator = value > 0; | ||
break; | ||
case 'm_iActiveDominations': | ||
playerResource.dominations = value; | ||
break; | ||
case 'm_flNextRespawnTime': | ||
playerResource.nextRespawn = value; | ||
break; | ||
case 'm_iChargeLevel': | ||
playerResource.chargeLevel = value; | ||
break; | ||
case 'm_iDamage': | ||
playerResource.damage = value; | ||
break; | ||
case 'm_iDamageAssist': | ||
playerResource.damageAssists = value; | ||
break; | ||
case 'm_iHealing': | ||
playerResource.healing = value; | ||
break; | ||
case 'm_iHealingAssist': | ||
playerResource.healingAssist = value; | ||
break; | ||
case 'm_iDamageBlocked': | ||
playerResource.damageBlocked = value; | ||
break; | ||
case 'm_iBonusPoints': | ||
playerResource.bonusPoints = value; | ||
break; | ||
case 'm_iPlayerLevel': | ||
playerResource.playerLevel = value; | ||
break; | ||
case 'm_iKillstreak': | ||
playerResource.killStreak = value; | ||
break; | ||
} | ||
} | ||
break; | ||
case 'CTeamRoundTimer': | ||
break; | ||
case 'CLaserDot': | ||
// for (const prop of entity.props) { | ||
// const propName = prop.definition.ownerTableName + '.' + prop.definition.name; | ||
// switch (propName) { | ||
// case 'DT_BaseEntity.m_iParentAttachment': | ||
// console.log(prop.value); | ||
// process.exit(); | ||
// break; | ||
// | ||
// } | ||
// } | ||
// console.log(match.getSendTable(entity.serverClass.dataTable).flattenedProps); | ||
break; | ||
} | ||
} | ||
function applyBuildingProp(building, prop, propName) { | ||
switch (propName) { | ||
case 'DT_BaseObject.m_iUpgradeLevel': | ||
building.level = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_hBuilder': | ||
building.builder = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_iMaxHealth': | ||
building.maxHealth = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_iHealth': | ||
building.health = prop.value; | ||
break; | ||
case 'DT_BaseObject.m_bBuilding': | ||
building.isBuilding = prop.value > 0; | ||
break; | ||
case 'DT_BaseObject.m_bHasSapper': | ||
building.isSapped = prop.value > 0; | ||
break; | ||
case 'DT_BaseEntity.m_vecOrigin': | ||
building.position = prop.value; | ||
break; | ||
case 'DT_BaseEntity.m_iTeamNum': | ||
building.team = prop.value; | ||
break; | ||
case 'DT_BaseEntity.m_angRotation': | ||
building.angle = prop.value.y; | ||
break; | ||
} | ||
} | ||
//# sourceMappingURL=PacketEntities.js.map |
@@ -38,5 +38,9 @@ export function handleStringTable(packet, state) { | ||
const name = extraData.readUTF8String(32); | ||
const userId = extraData.readUint32(); | ||
let userId = extraData.readUint32(); | ||
while (userId > 256) { | ||
userId -= 256; | ||
} | ||
const steamId = extraData.readUTF8String(); | ||
if (steamId) { | ||
const entityId = parseInt(text, 10) + 1; | ||
let userState = state.userInfo.get(userId); | ||
@@ -48,9 +52,8 @@ if (!userState) { | ||
steamId: '', | ||
entityId: 0 | ||
entityId | ||
}; | ||
state.userInfo.set(userState.userId, userState); | ||
} | ||
userState.name = name; | ||
userState.steamId = steamId; | ||
userState.entityId = parseInt(text, 10) + 1; | ||
state.userInfo.set(userId, userState); | ||
} | ||
@@ -57,0 +60,0 @@ } |
import { MessageType } from './Data/Message'; | ||
import { PacketTypeId } from './Data/Packet'; | ||
import { ParserState } from './Data/ParserState'; | ||
@@ -27,3 +28,9 @@ import { parseHeader } from './Parser/Header'; | ||
this.parserState = new ParserState(); | ||
this.parserState.skippedPackets = skipPackets; | ||
if (this.getHeader().game === 'hl2mp') { | ||
// for hl2dm we always need packet entities for team info and never tempEntities since it crashes the parser | ||
this.parserState.skippedPackets = [PacketTypeId.tempEntities]; | ||
} | ||
else { | ||
this.parserState.skippedPackets = skipPackets; | ||
} | ||
} | ||
@@ -30,0 +37,0 @@ getHeader() { |
@@ -31,3 +31,9 @@ import * as assert from 'assert'; | ||
}); | ||
test('Parse hl2dm ffa demo', () => { | ||
testDemo('hl2dm_ffa'); | ||
}); | ||
test('Parse hl2dm 2v2 demo', () => { | ||
testDemo('hl2dm_2v2'); | ||
}); | ||
}); | ||
//# sourceMappingURL=parse.js.map |
@@ -37,2 +37,4 @@ "use strict"; | ||
} | ||
player.team = prop.value; | ||
break; | ||
case 'DT_BasePlayer.m_iHealth': | ||
@@ -39,0 +41,0 @@ player.health = prop.value; |
{ | ||
"name": "@demostf/demo.js", | ||
"description": "A parser for TF2 demo files", | ||
"version": "2.1.0", | ||
"version": "2.1.1", | ||
"bin": { | ||
@@ -6,0 +6,0 @@ "demo-analyse": "./bin/analyse.js" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1265287
562
21018