mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: migration 2
This commit is contained in:
1 parent
e1c8194beb
commit
6285f33989
305 files changed
+414
-414
No files matched your search
@@ -8,9 +8,13 @@
|
||||
"lodash": "^4.17.21",
|
||||
"@prisma/client": "^5.19.1",
|
||||
"@octokit/plugin-throttling": "^8.2.0",
|
||||
"lz4js": "^0.2.0"
|
||||
"protobufjs": "^7.4.0",
|
||||
"lz4js": "^0.2.0",
|
||||
"pickleparser": "^0.2.1",
|
||||
"adm-zip": "^0.5.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/lz4js": "^0.2.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package BattleResultsGenerated;
|
||||
|
||||
import "PlayerAvatarImage.proto";
|
||||
|
||||
message Int32List {
|
||||
repeated int32 values = 1;
|
||||
}
|
||||
|
||||
message PlayerInfo {
|
||||
message AvatarEntry {
|
||||
int32 key = 1;
|
||||
.PlayerAvatarInfo value = 2;
|
||||
}
|
||||
|
||||
string name = 1;
|
||||
int32 prebattleID = 2;
|
||||
int32 team = 3;
|
||||
int64 clanID = 4;
|
||||
string clanTag = 5;
|
||||
repeated int32 chatMessageCount = 6;
|
||||
repeated .BattleResultsGenerated.PlayerInfo.AvatarEntry avatar = 7;
|
||||
int32 profileBackground = 8;
|
||||
int32 vehicleRank = 9;
|
||||
}
|
||||
|
||||
message TeamInfo {
|
||||
string title = 1;
|
||||
string icon = 2;
|
||||
}
|
||||
|
||||
message VehiclePublicResults {
|
||||
message ExtVehAchievementsEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
message ExtAccAchievementsEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
int32 health = 1;
|
||||
int32 credits = 2;
|
||||
int32 xp = 3;
|
||||
int32 shots = 4;
|
||||
int32 directHits = 5;
|
||||
int32 explosionHits = 6;
|
||||
int32 piercings = 7;
|
||||
int32 damageDealt = 8;
|
||||
int32 damageAssistedRadio = 9;
|
||||
int32 damageAssistedTrack = 10;
|
||||
int32 damageReceived = 11;
|
||||
int32 directHitsReceived = 12;
|
||||
int32 noDamageDirectHitsReceived = 13;
|
||||
int32 explosionHitsReceived = 14;
|
||||
int32 piercingsReceived = 15;
|
||||
int32 spotted = 16;
|
||||
int32 damaged = 17;
|
||||
int32 kills = 18;
|
||||
int32 tdamageDealt = 19;
|
||||
int32 tkills = 20;
|
||||
int32 capturePoints = 21;
|
||||
int32 droppedCapturePoints = 22;
|
||||
int32 mileage = 23;
|
||||
int32 lifeTime = 24;
|
||||
int32 killerID = 25;
|
||||
repeated int32 achievements = 26;
|
||||
repeated .BattleResultsGenerated.VehiclePublicResults.ExtVehAchievementsEntry extVehAchievements = 27;
|
||||
repeated .BattleResultsGenerated.VehiclePublicResults.ExtAccAchievementsEntry extAccAchievements = 28;
|
||||
int32 xpDamage = 29;
|
||||
int32 xpAssist = 30;
|
||||
int32 xpTeamBonus = 31;
|
||||
int32 wpEarned = 32;
|
||||
int32 wpStolen = 33;
|
||||
int32 heroCreditsBonus = 34;
|
||||
int32 heroXpBonus = 35;
|
||||
int64 accountDBID = 101;
|
||||
int32 team = 102;
|
||||
int32 serializedVehType = 103;
|
||||
int32 gold = 104;
|
||||
int32 deathReason = 105;
|
||||
int32 camouflage = 106;
|
||||
float mmr = 107;
|
||||
float mmrDiff = 108;
|
||||
int32 calibrationBattlesLeft = 109;
|
||||
int32 healedHP = 114;
|
||||
int32 wasHealedFor = 115;
|
||||
int32 xpActivity = 36;
|
||||
int32 customization = 116;
|
||||
int32 potentialEnemyDamage = 117;
|
||||
int32 rammingDamage = 118;
|
||||
int32 helpedToKill = 119;
|
||||
}
|
||||
|
||||
message BattleResultsFinalXp {
|
||||
int32 xpDamagePure = 1;
|
||||
int32 xpDamagePrem = 2;
|
||||
int32 xpAssistPure = 3;
|
||||
int32 xpAssistPrem = 4;
|
||||
int32 xpActivityPure = 5;
|
||||
int32 xpActivityPrem = 6;
|
||||
int32 xpTeamBonusPure = 7;
|
||||
int32 xpTeamBonusPrem = 8;
|
||||
int32 healingXpPure = 9;
|
||||
int32 healingXpPrem = 10;
|
||||
int32 heroXpBonusPure = 11;
|
||||
int32 heroXpBonusPrem = 12;
|
||||
int32 earnedFreeXpPure = 13;
|
||||
int32 earnedFreeXpPrem = 14;
|
||||
int32 earnedAvatarXpPure = 15;
|
||||
int32 earnedAvatarXpPrem = 16;
|
||||
int32 xpNoModifiers = 17;
|
||||
int32 xpPure = 18;
|
||||
int32 xpPrem = 19;
|
||||
int32 totalFreeXpPure = 20;
|
||||
int32 totalFreeXpPrem = 21;
|
||||
int32 totalAvatarXpPure = 22;
|
||||
int32 totalAvatarXpPrem = 23;
|
||||
}
|
||||
|
||||
message Ribbons {
|
||||
int32 id = 1;
|
||||
int32 grade = 2;
|
||||
int32 amount = 3;
|
||||
int32 value = 4;
|
||||
}
|
||||
|
||||
message BattleResultsUpdate {
|
||||
message ExcellentBattleDataEntry {
|
||||
string key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
message StuffEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
message RatingBattleStuffBonusEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
int32 avatarXP = 1;
|
||||
int32 eventCredits = 2;
|
||||
int32 eventGold = 3;
|
||||
int32 eventXP = 4;
|
||||
int32 eventFreeXP = 5;
|
||||
int32 eventAvatarXP = 6;
|
||||
int32 autoRepairCost = 7;
|
||||
repeated int32 autoLoadCost = 8;
|
||||
repeated int32 autoEquipCost = 9;
|
||||
repeated int32 autoProvCost = 10;
|
||||
bool isPremium = 12;
|
||||
int32 premiumXPFactor10 = 13;
|
||||
int32 premiumCreditsFactor10 = 14;
|
||||
int32 xpFactor10 = 15;
|
||||
int32 creditsFactor10 = 16;
|
||||
int32 aogasFactor10 = 17;
|
||||
int32 markOfMastery = 18;
|
||||
repeated .BattleResultsGenerated.BattleResultsUpdate.ExcellentBattleDataEntry excellentBattleData = 19;
|
||||
bytes vehDossier = 20;
|
||||
bytes accDossier = 21;
|
||||
bytes vehDossierPrev = 22;
|
||||
bytes accDossierPrev = 23;
|
||||
int32 autoRepairDiscount = 24;
|
||||
repeated int32 autoEquipDiscount = 25;
|
||||
repeated int32 autoProvDiscount = 26;
|
||||
repeated .BattleResultsGenerated.BattleResultsUpdate.StuffEntry stuff = 27;
|
||||
int32 boosterCredits = 29;
|
||||
int32 boosterXp = 30;
|
||||
int32 boosterFreeXp = 31;
|
||||
int32 boosterAvatarXp = 32;
|
||||
int32 complimentsLimit = 33;
|
||||
repeated int32 autoAbilsCost = 34;
|
||||
repeated int32 autoAbilsDiscount = 35;
|
||||
bool hasHealingReward = 36;
|
||||
int32 healingCredits = 37;
|
||||
int32 healingXp = 38;
|
||||
int32 healingFreeXp = 39;
|
||||
int32 healingAvatarXp = 40;
|
||||
float mmr = 107;
|
||||
float mmrDiff = 108;
|
||||
int32 calibrationBattlesLeft = 109;
|
||||
int32 ratingBattleXp = 110;
|
||||
int32 ratingBattleFreeXp = 111;
|
||||
int32 ratingBattleAvatarXp = 112;
|
||||
repeated .BattleResultsGenerated.BattleResultsUpdate.RatingBattleStuffBonusEntry ratingBattleStuffBonus = 113;
|
||||
repeated .BattleResultsGenerated.Ribbons ribbons = 114;
|
||||
}
|
||||
|
||||
message DropData {
|
||||
message StuffEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
int32 credits = 1;
|
||||
int32 gold = 2;
|
||||
int32 xp = 3;
|
||||
int32 freeXP = 4;
|
||||
int32 avatarXP = 5;
|
||||
repeated .BattleResultsGenerated.DropData.StuffEntry stuff = 27;
|
||||
}
|
||||
|
||||
message VehicleInteractionDetails {
|
||||
message DamagedModulesEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
message DestroyedModulesEntry {
|
||||
int32 key = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
bool spotted = 1;
|
||||
int32 deathReason = 2;
|
||||
int32 directHits = 3;
|
||||
int32 directHitWithoutDamage = 4;
|
||||
int32 explosionHits = 5;
|
||||
int32 piercings = 6;
|
||||
int32 damageDealt = 7;
|
||||
int32 damageAssistedTrack = 8;
|
||||
int32 damageAssistedRadio = 9;
|
||||
int32 fire = 10;
|
||||
repeated .BattleResultsGenerated.VehicleInteractionDetails.DamagedModulesEntry damagedModules = 11;
|
||||
repeated .BattleResultsGenerated.VehicleInteractionDetails.DestroyedModulesEntry destroyedModules = 12;
|
||||
int32 ricochetHitWithoutDamage = 13;
|
||||
}
|
||||
|
||||
message InteractionDetails {
|
||||
message VehiclesEntry {
|
||||
int32 key = 1;
|
||||
.BattleResultsGenerated.VehicleInteractionDetails value = 2;
|
||||
}
|
||||
|
||||
repeated .BattleResultsGenerated.InteractionDetails.VehiclesEntry vehicles = 1;
|
||||
}
|
||||
|
||||
message PrebattleStats {
|
||||
int32 freeXp = 1;
|
||||
int32 vehicleXp = 2;
|
||||
}
|
||||
|
||||
message BattleResultsClient {
|
||||
message QuestsProgressEntry {
|
||||
string key = 1;
|
||||
.BattleResultsGenerated.Int32List value = 2;
|
||||
}
|
||||
|
||||
message DropCategoriesEntry {
|
||||
string key = 1;
|
||||
.BattleResultsGenerated.DropData value = 2;
|
||||
}
|
||||
|
||||
message PlayersEntry {
|
||||
int64 key = 1;
|
||||
.BattleResultsGenerated.PlayerInfo value = 2;
|
||||
}
|
||||
|
||||
message VehiclesEntry {
|
||||
int32 key = 1;
|
||||
.BattleResultsGenerated.VehiclePublicResults value = 2;
|
||||
}
|
||||
|
||||
int32 arenaTypeID = 1;
|
||||
int32 arenaCreateTime = 2;
|
||||
int32 winnerTeam = 3;
|
||||
int32 finishReason = 4;
|
||||
int32 duration = 5;
|
||||
int32 bonusType = 6;
|
||||
int32 vehLockMode = 7;
|
||||
.BattleResultsGenerated.VehiclePublicResults public = 8;
|
||||
int32 gameModeID = 9;
|
||||
int32 observerMode = 10;
|
||||
.BattleResultsGenerated.BattleResultsFinalXp finalXp = 11;
|
||||
int32 repair = 136;
|
||||
int32 freeXP = 137;
|
||||
int32 xpPenalty = 145;
|
||||
int32 creditsPenalty = 146;
|
||||
int32 creditsContributionIn = 147;
|
||||
int32 creditsContributionOut = 148;
|
||||
.BattleResultsGenerated.BattleResultsUpdate update = 150;
|
||||
int32 originalXP = 181;
|
||||
int32 originalFreeXP = 182;
|
||||
int32 originalCredits = 183;
|
||||
repeated .BattleResultsGenerated.BattleResultsClient.QuestsProgressEntry questsProgress = 184;
|
||||
repeated .BattleResultsGenerated.BattleResultsClient.DropCategoriesEntry dropCategories = 185;
|
||||
.BattleResultsGenerated.TeamInfo teamInfo = 186;
|
||||
repeated .BattleResultsGenerated.BattleResultsClient.PlayersEntry players = 201;
|
||||
repeated .BattleResultsGenerated.BattleResultsClient.VehiclesEntry vehicles = 301;
|
||||
.BattleResultsGenerated.InteractionDetails interactionDetails = 302;
|
||||
.BattleResultsGenerated.PrebattleStats prebattleStats = 303;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message PlayerAvatarInfo {
|
||||
uint32 id = 1;
|
||||
string image = 2;
|
||||
string image_retina = 3;
|
||||
string grade = 4;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package blitzkit;
|
||||
|
||||
message AverageDefinitions {
|
||||
map<uint32, AverageDefinitionsEntry> averages = 1;
|
||||
Samples samples = 2;
|
||||
uint64 time = 3;
|
||||
}
|
||||
|
||||
message AverageDefinitionsEntry {
|
||||
AverageDefinitionsAllStats mu = 1;
|
||||
AverageDefinitionsAllStats sigma = 2;
|
||||
AverageDefinitionsAllStats r = 3;
|
||||
Samples samples = 4;
|
||||
}
|
||||
|
||||
message AverageDefinitionsAllStats {
|
||||
float battles = 1;
|
||||
float capture_points = 2;
|
||||
float damage_dealt = 3;
|
||||
float damage_received = 4;
|
||||
float dropped_capture_points = 5;
|
||||
float frags = 6;
|
||||
float frags8p = 7;
|
||||
float hits = 8;
|
||||
float losses = 9;
|
||||
float max_frags = 10;
|
||||
float max_xp = 11;
|
||||
float shots = 12;
|
||||
float spotted = 13;
|
||||
float survived_battles = 14;
|
||||
float win_and_survived = 15;
|
||||
float wins = 16;
|
||||
float xp = 17;
|
||||
float battle_life_time = 18;
|
||||
}
|
||||
|
||||
message Samples {
|
||||
uint32 d_1 = 1;
|
||||
uint32 d_7 = 2;
|
||||
uint32 d_30 = 3;
|
||||
uint32 d_60 = 4;
|
||||
uint32 d_90 = 5;
|
||||
uint32 d_120 = 6;
|
||||
uint32 total = 7;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package blitzkit;
|
||||
|
||||
message Reviews {
|
||||
map<uint32, Review> reviews = 1;
|
||||
}
|
||||
|
||||
message Review {
|
||||
float last_updated = 1;
|
||||
repeated Video videos = 2;
|
||||
}
|
||||
|
||||
message Video {
|
||||
string id = 1;
|
||||
string author = 2;
|
||||
}
|
||||
@@ -6,3 +6,23 @@ export const TANK_CLASSES = [
|
||||
'heavyTank',
|
||||
'AT-SPG',
|
||||
] as const;
|
||||
|
||||
export const emptyAllStats: BlitzStats = {
|
||||
battles: 0,
|
||||
capture_points: 0,
|
||||
damage_dealt: 0,
|
||||
damage_received: 0,
|
||||
dropped_capture_points: 0,
|
||||
frags: 0,
|
||||
frags8p: 0,
|
||||
hits: 0,
|
||||
losses: 0,
|
||||
max_frags: 0,
|
||||
max_xp: 0,
|
||||
shots: 0,
|
||||
spotted: 0,
|
||||
survived_battles: 0,
|
||||
xp: 0,
|
||||
win_and_survived: 0,
|
||||
wins: 0,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './constants';
|
||||
export * from './idToRegion';
|
||||
export * from './permanentSkills';
|
||||
export * from './resolveNearPenetration';
|
||||
@@ -0,0 +1,8 @@
|
||||
export const permanentSkills = [
|
||||
'smooth_turn',
|
||||
'soft_recoil',
|
||||
'smooth_driving',
|
||||
'smooth_turret',
|
||||
'virtuoso',
|
||||
'camouflage',
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ProvisionDefinitions } from '@blitzkit/core/src/blitzkit/provisionDefinitions';
|
||||
import { checkConsumableProvisionInclusivity } from './checkConsumableProvisionInclusivity';
|
||||
import { GunDefinition, TankDefinition } from './tankDefinitions';
|
||||
|
||||
export function availableProvisions(
|
||||
tank: TankDefinition,
|
||||
gun: GunDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
return Object.values(provisionDefinitions).filter((provision) =>
|
||||
checkConsumableProvisionInclusivity(provision, tank, gun),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Samples } from '.';
|
||||
import { emptyAllStats } from '../../blitz/constants';
|
||||
|
||||
export const emptyAverageDefinitionsAllStats: BlitzkitStats = {
|
||||
...emptyAllStats,
|
||||
battle_life_time: 0,
|
||||
};
|
||||
|
||||
export const averageDefinitionsAllStatsKeys = Object.keys(
|
||||
emptyAverageDefinitionsAllStats,
|
||||
) as (keyof BlitzkitStats)[];
|
||||
|
||||
export const emptySamples: Samples = {
|
||||
d_1: 0,
|
||||
d_7: 0,
|
||||
d_30: 0,
|
||||
d_60: 0,
|
||||
d_90: 0,
|
||||
d_120: 0,
|
||||
total: 0,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { BlitzkitStats } from '../../../../../src/core/statistics/compositeStats/constants';
|
||||
import { decodeProtobuf } from '../../protobuf/decodeProtobuf';
|
||||
import { asset } from '../asset';
|
||||
|
||||
export interface AverageDefinitionsEntry {
|
||||
mu: BlitzkitStats;
|
||||
sigma: BlitzkitStats;
|
||||
r: BlitzkitStats;
|
||||
samples: Samples;
|
||||
}
|
||||
|
||||
export interface AverageDefinitionsEntrySubPartial {
|
||||
mu: Partial<BlitzkitStats>;
|
||||
sigma: Partial<BlitzkitStats>;
|
||||
r: Partial<BlitzkitStats>;
|
||||
samples: Samples;
|
||||
}
|
||||
|
||||
export interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface AverageDefinitions {
|
||||
averages: Record<number, AverageDefinitionsEntry>;
|
||||
samples: Samples;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface Samples {
|
||||
d_1: number;
|
||||
d_7: number;
|
||||
d_30: number;
|
||||
d_60: number;
|
||||
d_90: number;
|
||||
d_120: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AverageDefinitionsManifest {
|
||||
version: 1;
|
||||
/**
|
||||
* epoch in days
|
||||
*/
|
||||
latest: number;
|
||||
}
|
||||
|
||||
export const averageDefinitions = fetch(asset('averages/manifest.json'), {
|
||||
cache: 'no-store',
|
||||
})
|
||||
.then((response) => response.json() as Promise<AverageDefinitionsManifest>)
|
||||
.then((manifest) => fetch(asset(`averages/${manifest.latest}.pb`)))
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((buffer) =>
|
||||
decodeProtobuf<AverageDefinitions>(
|
||||
'average_definitions',
|
||||
'blitzkit.AverageDefinitions',
|
||||
new Uint8Array(buffer),
|
||||
),
|
||||
);
|
||||
|
||||
export const averageDefinitionsArray = averageDefinitions.then((data) =>
|
||||
Object.entries(data.averages)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
({
|
||||
id: Number(key),
|
||||
...(value as AverageDefinitionsEntry),
|
||||
} satisfies AverageDefinitionsEntryWithId),
|
||||
)
|
||||
.filter((tank) => tank.mu.battles > 0),
|
||||
);
|
||||
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ConsumableEntry } from '@blitzkit/core/src/blitzkit/consumableDefinitions';
|
||||
import { ProvisionEntry } from '@blitzkit/core/src/blitzkit/provisionDefinitions';
|
||||
import { GunDefinition, TankDefinition } from './tankDefinitions';
|
||||
|
||||
export function checkConsumableProvisionInclusivity(
|
||||
consumableProvision: ConsumableEntry | ProvisionEntry,
|
||||
tank: TankDefinition,
|
||||
gun: GunDefinition,
|
||||
) {
|
||||
const included =
|
||||
!consumableProvision.gameMode &&
|
||||
consumableProvision.include?.every((rule) => {
|
||||
switch (rule.type) {
|
||||
case 'tier':
|
||||
return rule.min <= tank.tier && tank.tier <= rule.max;
|
||||
|
||||
case 'ids':
|
||||
return rule.ids.includes(tank.id);
|
||||
|
||||
case 'nation':
|
||||
return rule.nations.includes(tank.nation);
|
||||
|
||||
case 'category':
|
||||
throw new SyntaxError('Category filtering found in include rule');
|
||||
}
|
||||
});
|
||||
const excluded =
|
||||
consumableProvision.gameMode ||
|
||||
consumableProvision.exclude?.some((rule) => {
|
||||
switch (rule.type) {
|
||||
case 'tier':
|
||||
return rule.min <= tank.tier && tank.tier <= rule.max;
|
||||
|
||||
case 'ids':
|
||||
return rule.ids.includes(tank.id);
|
||||
|
||||
case 'nation':
|
||||
return rule.nations.includes(tank.nation);
|
||||
|
||||
case 'category':
|
||||
return rule.categories.some((category) => {
|
||||
switch (category) {
|
||||
case 'clip':
|
||||
return gun.type !== 'regular';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
!consumableProvision.gameMode &&
|
||||
consumableProvision.include?.length > 0 &&
|
||||
included &&
|
||||
!excluded
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function coefficient(...effects: [boolean, number][]) {
|
||||
return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 1);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { asset } from './asset';
|
||||
import { fetchCdonLz4 } from './fetchCdonLz4';
|
||||
|
||||
export type TankFilterDefinitionCategory = 'clip';
|
||||
|
||||
export type TankFilterDefinition =
|
||||
| {
|
||||
type: 'tier';
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
| {
|
||||
type: 'ids';
|
||||
ids: number[];
|
||||
}
|
||||
| {
|
||||
type: 'category';
|
||||
categories: TankFilterDefinitionCategory[];
|
||||
}
|
||||
| {
|
||||
type: 'nation';
|
||||
nations: string[];
|
||||
};
|
||||
|
||||
export type ConsumableEntry = {
|
||||
id: number;
|
||||
name: string;
|
||||
cooldown: number;
|
||||
duration?: number;
|
||||
} & (
|
||||
| {
|
||||
gameMode: false;
|
||||
include: TankFilterDefinition[];
|
||||
exclude?: TankFilterDefinition[];
|
||||
}
|
||||
| {
|
||||
gameMode: true;
|
||||
}
|
||||
);
|
||||
|
||||
export interface ConsumableDefinitions {
|
||||
[key: string]: ConsumableEntry;
|
||||
}
|
||||
|
||||
export const consumableDefinitions = fetchCdonLz4<ConsumableDefinitions>(
|
||||
asset('definitions/consumables.cdon.lz4'),
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SkillDefinitions } from '@blitzkit/core/src/blitzkit/skillDefinitions';
|
||||
import { permanentSkills } from '../blitz/permanentSkills';
|
||||
|
||||
export function createDefaultSkills(skillDefinitions: SkillDefinitions) {
|
||||
const skills: Record<string, number> = {};
|
||||
|
||||
Object.values(skillDefinitions.classes).forEach((tankSkills) => {
|
||||
tankSkills.forEach((skill) => {
|
||||
skills[skill] = permanentSkills.includes(skill) ? 7 : 0;
|
||||
});
|
||||
});
|
||||
|
||||
return skills;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { asset } from './asset';
|
||||
import { fetchCdonLz4 } from './fetchCdonLz4';
|
||||
|
||||
type EquipmentOptions = [number, number];
|
||||
|
||||
export type EquipmentRow = [
|
||||
EquipmentOptions,
|
||||
EquipmentOptions,
|
||||
EquipmentOptions,
|
||||
];
|
||||
|
||||
export type EquipmentPreset = [EquipmentRow, EquipmentRow, EquipmentRow];
|
||||
|
||||
export interface EquipmentDefinitions {
|
||||
presets: {
|
||||
[key: string]: EquipmentPreset;
|
||||
};
|
||||
equipments: {
|
||||
[key: number]: {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const equipmentDefinitions = fetchCdonLz4<EquipmentDefinitions>(
|
||||
asset('definitions/equipment.cdon.lz4'),
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
export class EventManager<Type> {
|
||||
private callbacks: Set<(data: Type) => void> = new Set();
|
||||
|
||||
on(callback: (data: Type) => void) {
|
||||
this.callbacks.add(callback);
|
||||
}
|
||||
off(callback: (data: Type) => void) {
|
||||
return this.callbacks.delete(callback);
|
||||
}
|
||||
|
||||
emit(data: Type) {
|
||||
this.callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,30 @@
|
||||
export * from './assertSecret';
|
||||
export * from './asset';
|
||||
export * from './availableProvisions';
|
||||
export * from './averageDefinitions';
|
||||
export * from './camouflageDefinitions';
|
||||
export * from './checkConsumableProvisionInclusivity';
|
||||
export * from './coefficient';
|
||||
export * from './consumableDefinitions';
|
||||
export * from './createDefaultSkills';
|
||||
export * from './equipmentDefinitions';
|
||||
export * from './eventManager';
|
||||
export * from './fetchCdonLz4';
|
||||
export * from './filterTank';
|
||||
export * from './gameDefinitions';
|
||||
export * from './isExplosive';
|
||||
export * from './modelDefinitions';
|
||||
export * from './pose';
|
||||
export * from './provisionDefinitions';
|
||||
export * from './resolvePenetrationCoefficient';
|
||||
export * from './reviews';
|
||||
export * from './skillDefinitions';
|
||||
export * from './sum';
|
||||
export * from './superCompress';
|
||||
export * from './superDecompress';
|
||||
export * from './tankCharacteristics';
|
||||
export * from './tankDefinitions';
|
||||
export * from './tankIcon';
|
||||
export * from './tankNames';
|
||||
export * from './uniqueGuns';
|
||||
export * from './youtubers';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ShellDefinition, ShellType } from '@blitzkit/core';
|
||||
|
||||
export function isExplosive(
|
||||
type: ShellDefinition['type'],
|
||||
): type is ShellType.HEAT | ShellType.HE {
|
||||
return type === ShellType.HEAT || type === ShellType.HE;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EventManager } from './eventManager';
|
||||
|
||||
export enum Pose {
|
||||
HullDown,
|
||||
FaceHug,
|
||||
Default,
|
||||
}
|
||||
|
||||
export const poseEvent = new EventManager<Pose>();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TankFilterDefinition } from '@blitzkit/core/src/blitzkit/consumableDefinitions';
|
||||
import { asset } from './asset';
|
||||
import { fetchCdonLz4 } from './fetchCdonLz4';
|
||||
|
||||
export type ProvisionEntry = {
|
||||
id: number;
|
||||
name: string;
|
||||
crew?: number;
|
||||
} & (
|
||||
| {
|
||||
gameMode: false;
|
||||
include: TankFilterDefinition[];
|
||||
exclude?: TankFilterDefinition[];
|
||||
}
|
||||
| {
|
||||
gameMode: true;
|
||||
}
|
||||
);
|
||||
|
||||
export interface ProvisionDefinitions {
|
||||
[key: string]: ProvisionEntry;
|
||||
}
|
||||
|
||||
export const provisionDefinitions = fetchCdonLz4<ProvisionDefinitions>(
|
||||
asset('definitions/provisions.cdon.lz4'),
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ShellType } from '../blitzkit/tankDefinitions';
|
||||
|
||||
export function resolvePenetrationCoefficient(
|
||||
calibratedShells: boolean,
|
||||
type: ShellType,
|
||||
) {
|
||||
if (!calibratedShells) return 1;
|
||||
|
||||
return type === 'ap'
|
||||
? 1.08
|
||||
: type === 'ap_cr'
|
||||
? 1.05
|
||||
: type === 'hc'
|
||||
? 1.13
|
||||
: 1.08;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { asset } from '@blitzkit/core/src/blitzkit/asset';
|
||||
import { decodeProtobuf } from '../protobuf';
|
||||
|
||||
export interface Reviews {
|
||||
reviews: Record<number, Review>;
|
||||
}
|
||||
|
||||
interface Review {
|
||||
last_updated: number;
|
||||
videos?: Video[];
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
id: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export const reviews = fetch(asset('definitions/reviews.pb'))
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((buffer) =>
|
||||
decodeProtobuf<Reviews>(
|
||||
'reviews',
|
||||
'blitzkit.Reviews',
|
||||
new Uint8Array(buffer),
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { asset } from '@blitzkit/core/src/blitzkit/asset';
|
||||
import { fetchCdonLz4 } from '@blitzkit/core/src/blitzkit/fetchCdonLz4';
|
||||
import { TankClass } from '../../../../src/components/Tanks';
|
||||
|
||||
export interface SkillDefinitions {
|
||||
classes: Record<TankClass, string[]>;
|
||||
}
|
||||
|
||||
export const skillDefinitions = fetchCdonLz4<SkillDefinitions>(
|
||||
asset('definitions/skills.cdon.lz4'),
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
export function sum(...effects: [boolean, number][]) {
|
||||
return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 0);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { compress } from 'lz4js';
|
||||
import { CdonWriteStream } from '../../../../src/core/streams/cdon';
|
||||
import { CdonWriteStream } from '../streams';
|
||||
|
||||
export function superCompress(object: any) {
|
||||
const write = new CdonWriteStream().cdon(object);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { decompress } from 'lz4js';
|
||||
import { CdonReadStream } from '../../../../src/core/streams/cdon';
|
||||
import { CdonReadStream } from '../streams';
|
||||
|
||||
export function superDecompress<Type>(buffer: ArrayBuffer) {
|
||||
const decompressed = decompress(new Uint8Array(buffer)).buffer as ArrayBuffer;
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
import { EquipmentDefinitions } from '@blitzkit/core/src/blitzkit/equipmentDefinitions';
|
||||
import { ModelDefinition } from '@blitzkit/core/src/blitzkit/modelDefinitions';
|
||||
import { ProvisionDefinitions } from '@blitzkit/core/src/blitzkit/provisionDefinitions';
|
||||
import { EquipmentMatrix } from '../../../website/src/stores/duel';
|
||||
import { resolveNearPenetration } from '../blitz/resolveNearPenetration';
|
||||
import { coefficient } from './coefficient';
|
||||
import { isExplosive } from './isExplosive';
|
||||
import { resolvePenetrationCoefficient } from './resolvePenetrationCoefficient';
|
||||
import { sum } from './sum';
|
||||
import {
|
||||
EngineDefinition,
|
||||
GunDefinition,
|
||||
ShellDefinition,
|
||||
TankDefinition,
|
||||
TrackDefinition,
|
||||
TurretDefinition,
|
||||
} from './tankDefinitions';
|
||||
|
||||
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
|
||||
|
||||
export function tankCharacteristics(
|
||||
{
|
||||
tank,
|
||||
turret,
|
||||
gun,
|
||||
shell,
|
||||
consumables,
|
||||
equipmentMatrix,
|
||||
crewMastery,
|
||||
provisions,
|
||||
engine,
|
||||
crewSkills,
|
||||
camouflage,
|
||||
track,
|
||||
stockEngine,
|
||||
stockTrack,
|
||||
stockTurret,
|
||||
stockGun,
|
||||
applyReactiveArmor,
|
||||
applyDynamicArmor,
|
||||
applySpallLiner,
|
||||
}: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
track: TrackDefinition;
|
||||
engine: EngineDefinition;
|
||||
stockEngine: EngineDefinition;
|
||||
stockTrack: TrackDefinition;
|
||||
stockTurret: TurretDefinition;
|
||||
stockGun: GunDefinition;
|
||||
consumables: number[];
|
||||
equipmentMatrix: EquipmentMatrix;
|
||||
crewMastery: number;
|
||||
provisions: number[];
|
||||
crewSkills: Record<string, number>;
|
||||
camouflage: boolean;
|
||||
applyReactiveArmor: boolean;
|
||||
applyDynamicArmor: boolean;
|
||||
applySpallLiner: boolean;
|
||||
},
|
||||
{
|
||||
tankModelDefinition,
|
||||
equipmentDefinitions,
|
||||
provisionDefinitions,
|
||||
}: {
|
||||
tankModelDefinition: ModelDefinition;
|
||||
equipmentDefinitions: EquipmentDefinitions;
|
||||
provisionDefinitions: ProvisionDefinitions;
|
||||
},
|
||||
) {
|
||||
const presetRows = equipmentDefinitions.presets[tank.equipment];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
function equipment(id: number) {
|
||||
return presetRows.some((equipmentRow, rowIndex) => {
|
||||
return equipmentRow.some((equipment, columnIndex) => {
|
||||
const choice = equipmentMatrix[rowIndex][columnIndex];
|
||||
if (choice === 0) return false;
|
||||
const equipped = equipment[choice === -1 ? 0 : 1];
|
||||
return equipped === id;
|
||||
});
|
||||
});
|
||||
}
|
||||
function consumable(id: number) {
|
||||
return consumables.includes(id);
|
||||
}
|
||||
function provision(id: number) {
|
||||
return provisions.includes(id);
|
||||
}
|
||||
|
||||
const hasGunRammer = equipment(100);
|
||||
const hasImprovedVentilation = equipment(102);
|
||||
const hasCalibratedShells = equipment(103);
|
||||
const hasEnhancedGunLayingDrive = equipment(104);
|
||||
const hasSupercharger = equipment(107);
|
||||
const hasVerticalStabilizer = equipment(105);
|
||||
const hasRefinedGun = equipment(106);
|
||||
const hasImprovedAssembly = equipment(111);
|
||||
const hasImprovedOptics = equipment(114);
|
||||
const hasImprovedControl = equipment(116);
|
||||
const hasEngineAccelerator = equipment(117);
|
||||
const hasCamouflageNet = equipment(115);
|
||||
const hasImprovedVerticalStabilizer = equipment(122);
|
||||
const hasImprovedSuspension = equipment(123);
|
||||
|
||||
const hasTungsten = consumable(45);
|
||||
const hasAdrenaline = consumable(18);
|
||||
const hasShellReloadBoost = consumable(29);
|
||||
const hasEnginePowerBoost = consumable(14);
|
||||
const hasImprovedEnginePowerBoost = consumable(27);
|
||||
const hasReticleCalibration = consumable(28);
|
||||
const hasReducedEnginePowerBoost = consumable(80);
|
||||
|
||||
const hasStandardFuel = provision(18);
|
||||
const hasImprovedFuel = provision(19);
|
||||
const hasProtectiveKit = provision(22);
|
||||
const hasSandbagArmor = provision(28);
|
||||
const hasEnhancedSandbagArmor = provision(29);
|
||||
const hasImprovedGunPowder = provision(46);
|
||||
const hasGearOil = provision(44);
|
||||
const hasImprovedGearOil = provision(45);
|
||||
|
||||
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0;
|
||||
const provisionCrewBonus =
|
||||
provisions.reduce(
|
||||
(total, provision) =>
|
||||
provisionDefinitions[provision].crew
|
||||
? total + provisionDefinitions[provision].crew! / 100
|
||||
: total,
|
||||
0,
|
||||
) + (hasImprovedVentilation ? 0.08 : 0);
|
||||
const commanderMastery = crewMastery + provisionCrewBonus;
|
||||
const loaderMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'loader') ? 1.1 : 1.05);
|
||||
const gunnerMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'gunner') ? 1.1 : 1.05);
|
||||
const driverMastery =
|
||||
commanderMastery *
|
||||
(tank.crew.some(({ type }) => type === 'driver') ? 1.1 : 1.05);
|
||||
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
|
||||
const damageCoefficient =
|
||||
coefficient([hasTungsten, 0.15]) *
|
||||
coefficient(
|
||||
[applyReactiveArmor, -0.27],
|
||||
[applyDynamicArmor, -0.1],
|
||||
[applySpallLiner && shell.type === 'he', -0.2],
|
||||
);
|
||||
const reloadCoefficient =
|
||||
(coefficient([hasGunRammer, -0.05]) *
|
||||
coefficient([true, degressiveStat(loaderMastery)])) /
|
||||
coefficient([hasAdrenaline && gun.type === 'regular', 0.17]);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const healthCoefficient = coefficient(
|
||||
[hasSandbagArmor, 0.03],
|
||||
[hasEnhancedSandbagArmor, 0.06],
|
||||
[hasImprovedAssembly, 0.04],
|
||||
);
|
||||
const shellVelocityCoefficient = coefficient(
|
||||
[hasSupercharger, 0.35],
|
||||
[hasImprovedGunPowder, 0.3],
|
||||
);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
const aimTimeCoefficient =
|
||||
coefficient(
|
||||
[hasEnhancedGunLayingDrive, -0.15],
|
||||
[hasReticleCalibration, -0.3],
|
||||
) * coefficient([true, degressiveStat(gunnerMastery)]);
|
||||
const dispersionStillCoefficient =
|
||||
coefficient([hasRefinedGun, -0.15], [hasReticleCalibration, -0.3]) *
|
||||
coefficient([true, degressiveStat(gunnerMastery)]);
|
||||
const dispersionDamagedCoefficient = coefficient([
|
||||
hasVerticalStabilizer,
|
||||
-0.12,
|
||||
]);
|
||||
const dispersionHullTraverseCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_turn / 100],
|
||||
);
|
||||
const dispersionAfterShotCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.soft_recoil / 100],
|
||||
);
|
||||
const dispersionMovingCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_driving / 100],
|
||||
);
|
||||
const dispersionTurretTraverseCoefficient = coefficient(
|
||||
[hasVerticalStabilizer, -0.15],
|
||||
[true, -crewSkills.smooth_turret * (2 / 100)],
|
||||
);
|
||||
const enginePowerCoefficient = coefficient(
|
||||
[
|
||||
hasEngineAccelerator,
|
||||
tank.class === 'heavyTank' || tank.class === 'AT-SPG' ? 0.05 : 0.04,
|
||||
],
|
||||
[hasReducedEnginePowerBoost, 0.1],
|
||||
[hasEnginePowerBoost, 0.2],
|
||||
[hasImprovedEnginePowerBoost, 0.35],
|
||||
[hasStandardFuel, 0.03],
|
||||
[hasImprovedFuel, 0.1],
|
||||
[hasGearOil, 0.03],
|
||||
[hasImprovedGearOil, 0.06],
|
||||
);
|
||||
const turretTraverseCoefficient =
|
||||
coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) *
|
||||
coefficient([true, progressiveStat(gunnerMastery)]);
|
||||
const hullTraverseCoefficient =
|
||||
coefficient(
|
||||
[hasImprovedControl, 0.11],
|
||||
[hasImprovedEnginePowerBoost, 0.01],
|
||||
[true, crewSkills.virtuoso / 100],
|
||||
) * coefficient([true, progressiveStat(driverMastery)]);
|
||||
const resistanceHardCoefficient = coefficient([hasImprovedSuspension, -0.2]);
|
||||
const resistanceMediumCoefficient = coefficient([
|
||||
hasImprovedSuspension,
|
||||
-0.15,
|
||||
]);
|
||||
const resistanceSoftCoefficient = coefficient([hasImprovedSuspension, -0.3]);
|
||||
const viewRangeCoefficient =
|
||||
coefficient([
|
||||
hasImprovedOptics,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.06
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.08
|
||||
: tank.class === 'lightTank'
|
||||
? 0.11
|
||||
: 0.04,
|
||||
]) * coefficient([true, progressiveStat(commanderMastery)]);
|
||||
const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]);
|
||||
|
||||
const speedForwardsSum = sum(
|
||||
[hasImprovedEnginePowerBoost, 4],
|
||||
[hasGearOil, 2],
|
||||
[hasImprovedGearOil, 4],
|
||||
);
|
||||
const speedBackwardsSum = sum(
|
||||
[hasImprovedEnginePowerBoost, 8],
|
||||
[hasGearOil, 2],
|
||||
[hasImprovedGearOil, 4],
|
||||
);
|
||||
const camouflageCoefficientMoving = sum(
|
||||
[
|
||||
true,
|
||||
coefficient([
|
||||
camouflage,
|
||||
tank.class === 'AT-SPG'
|
||||
? 0.04
|
||||
: tank.class === 'heavyTank'
|
||||
? 0.03
|
||||
: 0.02,
|
||||
]),
|
||||
],
|
||||
[
|
||||
hasCamouflageNet,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.02
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.04
|
||||
: tank.class === 'lightTank'
|
||||
? 0.07
|
||||
: 0.1,
|
||||
],
|
||||
);
|
||||
const camouflageCoefficientStill = sum(
|
||||
[
|
||||
true,
|
||||
coefficient(
|
||||
[
|
||||
camouflage,
|
||||
tank.class === 'AT-SPG'
|
||||
? 0.04
|
||||
: tank.class === 'heavyTank'
|
||||
? 0.03
|
||||
: 0.02,
|
||||
],
|
||||
[true, crewSkills.camouflage * (3 / 100)],
|
||||
),
|
||||
],
|
||||
[
|
||||
hasCamouflageNet,
|
||||
tank.class === 'heavyTank'
|
||||
? 0.04
|
||||
: tank.class === 'mediumTank'
|
||||
? 0.08
|
||||
: tank.class === 'lightTank'
|
||||
? 0.14
|
||||
: 0.2,
|
||||
],
|
||||
);
|
||||
|
||||
const size = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.boundingBox,
|
||||
turretModelDefinition.boundingBox,
|
||||
),
|
||||
);
|
||||
const weightKg =
|
||||
tank.weight + engine.weight + track.weight + turret.weight + gun.weight;
|
||||
const weightTons = weightKg / 1000;
|
||||
const stockWeight =
|
||||
tank.weight +
|
||||
stockEngine.weight +
|
||||
stockTrack.weight +
|
||||
stockTurret.weight +
|
||||
stockGun.weight;
|
||||
const resolvedEnginePower = engine.power * enginePowerCoefficient;
|
||||
const dpm = resolveDpm(
|
||||
gun,
|
||||
shell,
|
||||
damageCoefficient,
|
||||
reloadCoefficient,
|
||||
intraClipCoefficient,
|
||||
);
|
||||
const intraClip =
|
||||
gun.type === 'regular' ? undefined : gun.intraClip * intraClipCoefficient;
|
||||
const mostOptimalShellIndex =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload.reduce<null | { index: number; reload: number }>(
|
||||
(current, reloadRaw, index) => {
|
||||
const reload =
|
||||
reloadRaw * reloadCoefficient + (index > 0 ? intraClip! : 0);
|
||||
|
||||
if (current === null || reload < current.reload) {
|
||||
return { index, reload };
|
||||
}
|
||||
return current;
|
||||
},
|
||||
null,
|
||||
)!.index
|
||||
: undefined;
|
||||
const damage = shell.damage.armor * damageCoefficient;
|
||||
const dpmEffective =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload[0] > gun.reload[1] + gun.intraClip
|
||||
? ((damageCoefficient * shell.damage.armor) /
|
||||
(gun.reload.at(-1)! * reloadCoefficient +
|
||||
gun.intraClip * intraClipCoefficient)) *
|
||||
(60 -
|
||||
(gun.reload.slice(0, -1).length - 1) *
|
||||
gun.intraClip *
|
||||
intraClipCoefficient) +
|
||||
damageCoefficient *
|
||||
shell.damage.armor *
|
||||
gun.reload.slice(0, -1).length
|
||||
: ((damageCoefficient * shell.damage.armor) /
|
||||
(gun.reload[0] * reloadCoefficient +
|
||||
gun.intraClip * intraClipCoefficient)) *
|
||||
(60 -
|
||||
(gun.reload.slice(1).length - 1) *
|
||||
gun.intraClip *
|
||||
intraClipCoefficient) +
|
||||
damageCoefficient * shell.damage.armor * gun.reload.slice(1).length
|
||||
: undefined;
|
||||
const shells = gun.type === 'regular' ? 1 : gun.count;
|
||||
const shellReloads =
|
||||
gun.type === 'autoReloader'
|
||||
? gun.reload.map((reload) => reload * reloadCoefficient)
|
||||
: undefined;
|
||||
const shellReload =
|
||||
gun.type === 'autoReloader' ? undefined : gun.reload * reloadCoefficient;
|
||||
const caliber = shell.caliber;
|
||||
const penetration =
|
||||
resolveNearPenetration(shell.penetration) * penetrationCoefficient;
|
||||
const clipDamage = gun.type === 'regular' ? undefined : gun.count * damage;
|
||||
const moduleDamage = shell.damage.module * damageCoefficient;
|
||||
const explosionRadius = shell.explosionRadius;
|
||||
const shellVelocity = shell.speed * shellVelocityCoefficient;
|
||||
const aimTime = gun.aimTime * aimTimeCoefficient;
|
||||
const dispersion = gun.dispersion.base * dispersionStillCoefficient;
|
||||
const maxDispersion =
|
||||
gun.dispersion.base *
|
||||
Math.sqrt(
|
||||
((gun.dispersion.traverse * turret.traverseSpeed) ** 2 +
|
||||
(track.dispersion.traverse * track.traverseSpeed) ** 2 +
|
||||
track.dispersion.move * tank.speed.forwards) **
|
||||
2,
|
||||
);
|
||||
const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient;
|
||||
const dispersionHullTraversing =
|
||||
track.dispersion.traverse * dispersionHullTraverseCoefficient;
|
||||
const dispersionTurretTraversing =
|
||||
gun.dispersion.traverse * dispersionTurretTraverseCoefficient;
|
||||
const dispersionShooting =
|
||||
gun.dispersion.shot * dispersionAfterShotCoefficient;
|
||||
const dispersionGunDamaged =
|
||||
gun.dispersion.damaged * dispersionDamagedCoefficient;
|
||||
const gunDepression =
|
||||
gunModelDefinition.pitch.max +
|
||||
gunDefaultPitch +
|
||||
(hasImprovedVerticalStabilizer ? 3 : 0);
|
||||
const gunElevation =
|
||||
-gunModelDefinition.pitch.min -
|
||||
gunDefaultPitch +
|
||||
(hasImprovedVerticalStabilizer ? 4 : 0);
|
||||
const gunFrontalDepression = gunModelDefinition.pitch.front?.max;
|
||||
const gunFrontalElevation = gunModelDefinition.pitch.front
|
||||
? -gunModelDefinition.pitch.front.min
|
||||
: undefined;
|
||||
const gunRearDepression = gunModelDefinition.pitch.back?.max;
|
||||
const gunRearElevation = gunModelDefinition.pitch.back
|
||||
? -gunModelDefinition.pitch.back.min
|
||||
: undefined;
|
||||
const azimuthLeft = turretModelDefinition.yaw
|
||||
? -turretModelDefinition.yaw.min
|
||||
: undefined;
|
||||
const azimuthRight = turretModelDefinition.yaw?.max;
|
||||
const speedForwards = tank.speed.forwards + speedForwardsSum;
|
||||
const speedBackwards = tank.speed.backwards + speedBackwardsSum;
|
||||
const enginePower = resolvedEnginePower;
|
||||
const hardTerrainCoefficientRaw =
|
||||
track.resistance.hard * resistanceHardCoefficient;
|
||||
const hardTerrainCoefficient = 100 / hardTerrainCoefficientRaw;
|
||||
const mediumTerrainCoefficientRaw =
|
||||
track.resistance.medium * resistanceMediumCoefficient;
|
||||
const mediumTerrainCoefficient = 100 / mediumTerrainCoefficientRaw;
|
||||
const softTerrainCoefficientRaw =
|
||||
track.resistance.soft * resistanceSoftCoefficient;
|
||||
const softTerrainCoefficient = 100 / softTerrainCoefficientRaw;
|
||||
const powerToWeightRatioHardTerrain =
|
||||
resolvedEnginePower / weightTons / hardTerrainCoefficientRaw;
|
||||
const powerToWeightRatioMediumTerrain =
|
||||
resolvedEnginePower / weightTons / mediumTerrainCoefficientRaw;
|
||||
const powerToWeightRatioSoftTerrain =
|
||||
resolvedEnginePower / weightTons / softTerrainCoefficientRaw;
|
||||
const weight = weightTons;
|
||||
const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient;
|
||||
const hullTraverseHardTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / hardTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const hullTraverseMediumTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / mediumTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const hullTraverseSoftTerrain =
|
||||
(resolvedEnginePower / stockEngine.power) *
|
||||
track.traverseSpeed *
|
||||
hullTraverseCoefficient *
|
||||
(track.resistance.hard / softTerrainCoefficientRaw) *
|
||||
(stockWeight / weightKg);
|
||||
const health = (tank.health + turret.health) * healthCoefficient;
|
||||
const fireChance = engine.fireChance * fireChanceCoefficient;
|
||||
const viewRange = turret.viewRange * viewRangeCoefficient;
|
||||
const camouflageStill = tank.camouflage.still * camouflageCoefficientStill;
|
||||
const camouflageMoving =
|
||||
tank.class === 'lightTank'
|
||||
? tank.camouflage.still * camouflageCoefficientStill
|
||||
: tank.camouflage.moving * camouflageCoefficientMoving;
|
||||
const camouflageShootingStill =
|
||||
tank.camouflage.still * gun.camouflageLoss * camouflageCoefficientStill;
|
||||
const camouflageShootingMoving =
|
||||
(tank.class === 'lightTank'
|
||||
? tank.camouflage.still * camouflageCoefficientStill
|
||||
: tank.camouflage.moving * camouflageCoefficientMoving) *
|
||||
gun.camouflageLoss;
|
||||
const camouflageCaughtOnFire =
|
||||
tank.camouflage.onFire * tank.camouflage.still * camouflageCoefficientStill;
|
||||
const width = size[2];
|
||||
const height = size[0];
|
||||
const length = size[1];
|
||||
const volume = width * height * length;
|
||||
const shellNormalization = shell.normalization ?? 0;
|
||||
const shellRicochet = isExplosive(shell.type) ? undefined : shell.ricochet;
|
||||
|
||||
return {
|
||||
shellNormalization,
|
||||
shellRicochet,
|
||||
dpm,
|
||||
dpmEffective,
|
||||
shells,
|
||||
mostOptimalShellIndex,
|
||||
shellReloads,
|
||||
shellReload,
|
||||
intraClip,
|
||||
caliber,
|
||||
penetration,
|
||||
damage,
|
||||
clipDamage,
|
||||
moduleDamage,
|
||||
explosionRadius,
|
||||
shellVelocity,
|
||||
aimTime,
|
||||
dispersion,
|
||||
maxDispersion,
|
||||
dispersionMoving,
|
||||
dispersionHullTraversing,
|
||||
dispersionTurretTraversing,
|
||||
dispersionShooting,
|
||||
dispersionGunDamaged,
|
||||
gunDepression,
|
||||
gunElevation,
|
||||
gunFrontalDepression,
|
||||
gunFrontalElevation,
|
||||
gunRearDepression,
|
||||
gunRearElevation,
|
||||
azimuthLeft,
|
||||
azimuthRight,
|
||||
speedForwards,
|
||||
speedBackwards,
|
||||
hardTerrainCoefficient,
|
||||
hardTerrainCoefficientRaw,
|
||||
mediumTerrainCoefficient,
|
||||
mediumTerrainCoefficientRaw,
|
||||
softTerrainCoefficient,
|
||||
softTerrainCoefficientRaw,
|
||||
enginePower,
|
||||
powerToWeightRatioHardTerrain,
|
||||
powerToWeightRatioMediumTerrain,
|
||||
powerToWeightRatioSoftTerrain,
|
||||
weight,
|
||||
turretTraverseSpeed,
|
||||
hullTraverseHardTerrain,
|
||||
hullTraverseMediumTerrain,
|
||||
hullTraverseSoftTerrain,
|
||||
health,
|
||||
fireChance,
|
||||
viewRange,
|
||||
camouflageStill,
|
||||
camouflageMoving,
|
||||
camouflageShootingStill,
|
||||
camouflageShootingMoving,
|
||||
camouflageCaughtOnFire,
|
||||
width,
|
||||
height,
|
||||
length,
|
||||
volume,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { forEachRight } from 'lodash';
|
||||
import { GunDefinition, TurretDefinition } from './tankDefinitions';
|
||||
|
||||
export function uniqueGuns(turrets: TurretDefinition[]) {
|
||||
const gunIds = new Set<number>();
|
||||
const guns: { turret: TurretDefinition; gun: GunDefinition }[] = [];
|
||||
|
||||
forEachRight(turrets, (turret) => {
|
||||
forEachRight(turret.guns, (gun) => {
|
||||
if (!gunIds.has(gun.id)) {
|
||||
gunIds.add(gun.id);
|
||||
guns.push({ turret, gun });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return guns;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const youtubers = [
|
||||
{ id: 'UC9lE09jxmMgMoG13WrNHPmA', name: 'World of Tanks Blitz' },
|
||||
{ id: 'UCrQ-Dy8lVsm11u8pCJ8W7Tw', name: 'HisRoyalFatness' },
|
||||
{ id: 'UCxRpZqMElssTdPYDK3x7wyg', name: 'N90' },
|
||||
];
|
||||
@@ -1,5 +1,10 @@
|
||||
export * from './blitz';
|
||||
export * from './blitzkit';
|
||||
export * from './imgur';
|
||||
export * from './math';
|
||||
export * from './patreon';
|
||||
export * from './protobuf';
|
||||
export * from './radix';
|
||||
export * from './statistics';
|
||||
export * from './streams';
|
||||
export * from './zustand';
|
||||
@@ -0,0 +1,3 @@
|
||||
export function NaNFallback(value: number, fallback: number) {
|
||||
return isNaN(value) ? fallback : value;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export const ZERO_VECTOR = new Vector3();
|
||||
export const I_HAT = new Vector3(1, 0, 0);
|
||||
export const J_HAT = new Vector3(0, 1, 0);
|
||||
export const K_HAT = new Vector3(0, 0, 1);
|
||||
@@ -0,0 +1,5 @@
|
||||
const numberFormat = Intl.NumberFormat('en', { notation: 'compact' });
|
||||
|
||||
export function formatCompact(number: number) {
|
||||
return numberFormat.format(number);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './axis';
|
||||
export * from './formatCompact';
|
||||
export * from './isNumber';
|
||||
export * from './NaNFallback';
|
||||
export * from './normalizeAngleRad';
|
||||
export * from './normalToEuler';
|
||||
export * from './numberToRomanNumeral';
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function isNumber(value: any) {
|
||||
return typeof value === 'number' && isFinite(value);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Euler, Quaternion, Vector3 } from 'three';
|
||||
import { J_HAT } from './axis';
|
||||
|
||||
const quaternion = new Quaternion();
|
||||
const axis = new Vector3();
|
||||
|
||||
export function normalToEuler(normal: Vector3, up = J_HAT) {
|
||||
return new Euler().setFromQuaternion(
|
||||
quaternion.setFromAxisAngle(
|
||||
axis.crossVectors(up, normal).normalize(),
|
||||
up.angleTo(normal),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function normalizeAngleRad(angle: number) {
|
||||
const newAngle = angle % (2 * Math.PI);
|
||||
if (newAngle > Math.PI) return newAngle - 2 * Math.PI;
|
||||
if (newAngle < -Math.PI) return newAngle + 2 * Math.PI;
|
||||
return newAngle;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Thanks, August!
|
||||
* https://stackoverflow.com/a/41358305/12294756
|
||||
*/
|
||||
const ROMAN = {
|
||||
M: 1000,
|
||||
CM: 900,
|
||||
D: 500,
|
||||
CD: 400,
|
||||
C: 100,
|
||||
XC: 90,
|
||||
L: 50,
|
||||
XL: 40,
|
||||
X: 10,
|
||||
IX: 9,
|
||||
V: 5,
|
||||
IV: 4,
|
||||
I: 1,
|
||||
};
|
||||
const ROMAN_KEYS = Object.keys(ROMAN) as (keyof typeof ROMAN)[];
|
||||
|
||||
export default function numberToRomanNumeral(value: number) {
|
||||
let string = '';
|
||||
|
||||
ROMAN_KEYS.forEach((index) => {
|
||||
const part = Math.floor(value / ROMAN[index]);
|
||||
value -= part * ROMAN[index];
|
||||
string += index.repeat(part);
|
||||
});
|
||||
|
||||
return string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { lookupProtobufType, ProtoSource } from './lookupProtobufType';
|
||||
|
||||
export function decodeProtobuf<Type>(
|
||||
source: ProtoSource,
|
||||
type: string,
|
||||
data: Uint8Array,
|
||||
) {
|
||||
return lookupProtobufType(source, type).decode(data).toJSON() as Type;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { lookupProtobufType, ProtoSource } from './lookupProtobufType';
|
||||
|
||||
export function encodeProtobuf(
|
||||
source: ProtoSource,
|
||||
type: string,
|
||||
data: Record<string, any>,
|
||||
) {
|
||||
const message = lookupProtobufType(source, type);
|
||||
const verification = message.verify(data);
|
||||
|
||||
if (verification !== null) throw new Error(verification);
|
||||
|
||||
return message.encode(message.create(data)).finish();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { encodeProtobuf } from './encodeProtobuf';
|
||||
import { ProtoSource } from './lookupProtobufType';
|
||||
|
||||
export async function encodeProtobufToBase64(
|
||||
source: ProtoSource,
|
||||
type: string,
|
||||
data: any,
|
||||
) {
|
||||
const array = encodeProtobuf(source, type, data);
|
||||
return Buffer.from(array).toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './decodeProtobuf';
|
||||
export * from './encodeProtobuf';
|
||||
export * from './encodeProtobufToBase64';
|
||||
export * from './lookupProtobufType';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { parse } from 'protobufjs';
|
||||
import average_definitions from '../../protos/average_definitions.proto';
|
||||
import reviews from '../../protos/reviews.proto';
|
||||
|
||||
export type ProtoSource = keyof typeof protos;
|
||||
|
||||
const protos = { reviews, average_definitions };
|
||||
|
||||
export function lookupProtobufType(source: ProtoSource, type: string) {
|
||||
return parse(protos[source]).root.lookupType(type);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './var';
|
||||
@@ -0,0 +1,8 @@
|
||||
type Name =
|
||||
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||
| `radius-${1 | 2 | 3 | 4 | 5 | 6 | 'full'}`
|
||||
| `space-${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
|
||||
|
||||
export function Var(name: Name) {
|
||||
return `var(--${name})`;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { BlitzStats } from './compositeStats/constants';
|
||||
|
||||
/**
|
||||
* Formula from https://web.archive.org/web/20190105064817/http://wiki.wnefficiency.net/pages/WN8
|
||||
*
|
||||
* @param expected what all players on average perform like
|
||||
* @param average what the player in question performs like
|
||||
*/
|
||||
export default function calculateWN8(
|
||||
expected: BlitzStats,
|
||||
average: {
|
||||
damage_dealt: number;
|
||||
battles: number;
|
||||
spotted: number;
|
||||
frags: number;
|
||||
dropped_capture_points: number;
|
||||
wins: number;
|
||||
},
|
||||
) {
|
||||
const avgDmg = average.damage_dealt / average.battles;
|
||||
const avgSpot = average.spotted / average.battles;
|
||||
const avgFrag = average.frags / average.battles;
|
||||
const avgDef = average.dropped_capture_points / average.battles;
|
||||
const avgWinRate = average.wins / average.battles;
|
||||
|
||||
const expDmg = expected.damage_dealt / expected.battles;
|
||||
const expSpot = expected.spotted / expected.battles;
|
||||
const expFrag = expected.frags / expected.battles;
|
||||
const expDef = expected.dropped_capture_points / expected.battles;
|
||||
const expWinRate = expected.wins / expected.battles;
|
||||
|
||||
const rDAMAGE = avgDmg / expDmg;
|
||||
const rSPOT = avgSpot / expSpot;
|
||||
const rFRAG = avgFrag / expFrag;
|
||||
const rDEF = avgDef / expDef;
|
||||
const rWIN = avgWinRate / expWinRate;
|
||||
|
||||
const rWINc = Math.max(0, (rWIN - 0.71) / (1 - 0.71));
|
||||
const rDAMAGEc = Math.max(0, (rDAMAGE - 0.22) / (1 - 0.22));
|
||||
const rFRAGc = Math.max(
|
||||
0,
|
||||
Math.min(rDAMAGEc + 0.2, (rFRAG - 0.12) / (1 - 0.12)),
|
||||
);
|
||||
const rSPOTc = Math.max(
|
||||
0,
|
||||
Math.min(rDAMAGEc + 0.1, (rSPOT - 0.38) / (1 - 0.38)),
|
||||
);
|
||||
const rDEFc = Math.max(0, Math.min(rDAMAGEc + 0.1, (rDEF - 0.1) / (1 - 0.1)));
|
||||
|
||||
const WN8 =
|
||||
980 * rDAMAGEc +
|
||||
210 * rDAMAGEc * rFRAGc +
|
||||
155 * rFRAGc * rSPOTc +
|
||||
75 * rDEFc * rFRAGc +
|
||||
145 * Math.min(1.8, rWINc);
|
||||
|
||||
return WN8;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
export const blitzStatsKeys = [
|
||||
'battles',
|
||||
'capture_points',
|
||||
'damage_dealt',
|
||||
'damage_received',
|
||||
'dropped_capture_points',
|
||||
'frags',
|
||||
'frags8p',
|
||||
'hits',
|
||||
'losses',
|
||||
'max_frags',
|
||||
'max_xp',
|
||||
'shots',
|
||||
'spotted',
|
||||
'survived_battles',
|
||||
'win_and_survived',
|
||||
'wins',
|
||||
'xp',
|
||||
] as const;
|
||||
|
||||
export const blitzkitStatsKeys = [
|
||||
...blitzStatsKeys,
|
||||
'battle_life_time',
|
||||
] as const;
|
||||
|
||||
export const blitzPlayerStatsKeys = [
|
||||
...blitzStatsKeys,
|
||||
'max_frags_tank_id',
|
||||
'max_xp_tank_id',
|
||||
] as const;
|
||||
|
||||
export const compositeStatsKeysBase = [
|
||||
...blitzkitStatsKeys,
|
||||
'damage_ratio',
|
||||
'kills_to_death_ratio',
|
||||
'accuracy',
|
||||
'inaccuracy',
|
||||
'misses',
|
||||
'deaths',
|
||||
'wn8',
|
||||
] as const;
|
||||
|
||||
const normalizedBlitzStatsKeys = [
|
||||
// 'battles', will result in 1
|
||||
// 'capture_points', not all battles are encounter mode
|
||||
'damage_dealt',
|
||||
'damage_received',
|
||||
// 'dropped_capture_points', not all battles are encounter mode
|
||||
'frags',
|
||||
// 'frags8p', null on most tanks and not all battles have tier 8 and above tanks
|
||||
'hits',
|
||||
'losses',
|
||||
// 'max_frags', counts the max; not cumulative
|
||||
// 'max_xp', counts the max; not cumulative
|
||||
'shots',
|
||||
'spotted',
|
||||
'survived_battles',
|
||||
'win_and_survived',
|
||||
'wins',
|
||||
'xp',
|
||||
] satisfies (typeof blitzStatsKeys)[number][];
|
||||
|
||||
const normalizedBlitzkitStatsKeys = [
|
||||
...normalizedBlitzStatsKeys,
|
||||
'battle_life_time',
|
||||
] satisfies (typeof blitzkitStatsKeys)[number][];
|
||||
|
||||
const cumulativeBlitzStatsKeys = [
|
||||
'battles',
|
||||
'capture_points',
|
||||
'damage_dealt',
|
||||
'damage_received',
|
||||
'dropped_capture_points',
|
||||
'frags',
|
||||
// 'frags8p', once again, just a painful metric
|
||||
'hits',
|
||||
'losses',
|
||||
// 'max_frags', static, not cumulative
|
||||
// 'max_xp', static, not cumulative
|
||||
'shots',
|
||||
'spotted',
|
||||
'survived_battles',
|
||||
'win_and_survived',
|
||||
'wins',
|
||||
'xp',
|
||||
] satisfies (typeof blitzStatsKeys)[number][];
|
||||
|
||||
const cumulativeBlitzkitStatsKeys = [
|
||||
...cumulativeBlitzStatsKeys,
|
||||
'battle_life_time',
|
||||
] satisfies (typeof blitzkitStatsKeys)[number][];
|
||||
|
||||
const normalizedCompositeStatsKeys = [
|
||||
...normalizedBlitzkitStatsKeys,
|
||||
// 'damage_ratio', is a dimensionless ratio
|
||||
// 'kills_to_death_ratio', also a dimensionless ratio
|
||||
// 'accuracy', dimensionless ratio
|
||||
// 'inaccuracy', dimensionless ratio
|
||||
'misses',
|
||||
'deaths',
|
||||
// 'wn8', is based on a sample of all battles so doesn't make sense to normalize
|
||||
] satisfies (typeof compositeStatsKeysBase)[number][];
|
||||
|
||||
const cumulativeCompositeStatsKeys = [
|
||||
...cumulativeBlitzkitStatsKeys,
|
||||
'damage_ratio',
|
||||
'kills_to_death_ratio',
|
||||
'accuracy',
|
||||
'inaccuracy',
|
||||
'misses',
|
||||
'deaths',
|
||||
'wn8',
|
||||
] satisfies (typeof compositeStatsKeysBase)[number][];
|
||||
|
||||
export const compositeStatsKeys = [
|
||||
...normalizedCompositeStatsKeys.map((key) => `normalized_${key}`),
|
||||
...cumulativeCompositeStatsKeys.map((key) => `cumulative_${key}`),
|
||||
] as CompositeStatsKey[];
|
||||
|
||||
type BlitzStatsKey = (typeof blitzStatsKeys)[number];
|
||||
export type BlitzStats = Record<BlitzStatsKey, number>;
|
||||
|
||||
type BlitzkitStatsKey = (typeof blitzkitStatsKeys)[number];
|
||||
export type BlitzkitStats = Record<BlitzkitStatsKey, number>;
|
||||
|
||||
type BlitzPlayerStatsKey = (typeof blitzPlayerStatsKeys)[number];
|
||||
export type BlitzPlayerStats = Record<BlitzPlayerStatsKey, number>;
|
||||
|
||||
export type CompositeStatsKey =
|
||||
| `normalized_${(typeof normalizedCompositeStatsKeys)[number]}`
|
||||
| `cumulative_${(typeof cumulativeCompositeStatsKeys)[number]}`;
|
||||
export type CompositeStats = Record<CompositeStatsKey, number>;
|
||||
|
||||
export const compositeStatsFormatting: {
|
||||
[Key in CompositeStatsKey]: {
|
||||
unit?: string;
|
||||
fixed: number;
|
||||
coefficient?: number;
|
||||
preview: number;
|
||||
localeFormat?: boolean;
|
||||
};
|
||||
} = {
|
||||
normalized_damage_dealt: { fixed: 0, preview: 3246, localeFormat: true },
|
||||
normalized_damage_received: { fixed: 0, preview: 966, localeFormat: true },
|
||||
normalized_deaths: { fixed: 0, unit: '%', coefficient: 100, preview: 0.09 },
|
||||
normalized_frags: { fixed: 2, preview: 2.1 },
|
||||
normalized_hits: { fixed: 2, preview: 12.33 },
|
||||
normalized_losses: { fixed: 0, unit: '%', coefficient: 100, preview: 0.3 },
|
||||
normalized_misses: { fixed: 2, preview: 1.02 },
|
||||
normalized_shots: { fixed: 2, preview: 14.54 },
|
||||
normalized_spotted: { fixed: 2, preview: 2.02 },
|
||||
normalized_survived_battles: {
|
||||
fixed: 0,
|
||||
unit: '%',
|
||||
coefficient: 100,
|
||||
preview: 0.65,
|
||||
},
|
||||
normalized_win_and_survived: {
|
||||
fixed: 0,
|
||||
unit: '%',
|
||||
coefficient: 100,
|
||||
preview: 0.59,
|
||||
},
|
||||
normalized_wins: { fixed: 0, unit: '%', coefficient: 100, preview: 0.72 },
|
||||
normalized_xp: { fixed: 0, preview: 982, localeFormat: true },
|
||||
normalized_battle_life_time: { fixed: 1, preview: 4.6, coefficient: 1 / 60 },
|
||||
|
||||
cumulative_accuracy: { fixed: 0, unit: '%', coefficient: 100, preview: 0.91 },
|
||||
cumulative_battles: { fixed: 0, preview: 12 },
|
||||
cumulative_capture_points: { fixed: 2, preview: 0.12 },
|
||||
cumulative_damage_dealt: { fixed: 0, preview: 45023, localeFormat: true },
|
||||
cumulative_damage_ratio: { fixed: 2, preview: 2.83 },
|
||||
cumulative_damage_received: { fixed: 0, preview: 10093, localeFormat: true },
|
||||
cumulative_deaths: { fixed: 0, preview: 2 },
|
||||
cumulative_dropped_capture_points: { fixed: 2, preview: 15 },
|
||||
cumulative_frags: { fixed: 0, preview: 26 },
|
||||
cumulative_hits: { fixed: 0, preview: 480 },
|
||||
cumulative_inaccuracy: {
|
||||
fixed: 0,
|
||||
unit: '%',
|
||||
coefficient: 100,
|
||||
preview: 0.08,
|
||||
},
|
||||
cumulative_kills_to_death_ratio: { fixed: 2, preview: 2.3 },
|
||||
cumulative_losses: { fixed: 0, preview: 3 },
|
||||
cumulative_misses: { fixed: 0, preview: 13 },
|
||||
cumulative_shots: { fixed: 0, preview: 309 },
|
||||
cumulative_spotted: { fixed: 0, preview: 99 },
|
||||
cumulative_survived_battles: { fixed: 0, preview: 10 },
|
||||
cumulative_win_and_survived: { fixed: 0, preview: 9 },
|
||||
cumulative_wins: { fixed: 0, preview: 8 },
|
||||
cumulative_xp: { fixed: 0, preview: 19403, localeFormat: true },
|
||||
cumulative_wn8: { fixed: 0, preview: 3103, localeFormat: true },
|
||||
cumulative_battle_life_time: {
|
||||
fixed: 0,
|
||||
preview: 109,
|
||||
coefficient: 1 / 60,
|
||||
localeFormat: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import calculateWN8 from '../calculateWN8';
|
||||
import {
|
||||
BlitzkitStats,
|
||||
CompositeStats,
|
||||
compositeStatsFormatting,
|
||||
CompositeStatsKey,
|
||||
} from './constants';
|
||||
|
||||
export function compositeStats(stats: BlitzkitStats, average: BlitzkitStats) {
|
||||
return {
|
||||
normalized_damage_dealt: stats.damage_dealt / stats.battles,
|
||||
normalized_damage_received: stats.damage_received / stats.battles,
|
||||
normalized_deaths: (stats.battles - stats.survived_battles) / stats.battles,
|
||||
normalized_frags: stats.frags / stats.battles,
|
||||
normalized_hits: stats.hits / stats.battles,
|
||||
normalized_losses: stats.losses / stats.battles,
|
||||
normalized_misses: (stats.shots - stats.hits) / stats.battles,
|
||||
normalized_shots: stats.shots / stats.battles,
|
||||
normalized_spotted: stats.spotted / stats.battles,
|
||||
normalized_survived_battles: stats.survived_battles / stats.battles,
|
||||
normalized_win_and_survived: stats.win_and_survived / stats.battles,
|
||||
normalized_wins: stats.wins / stats.battles,
|
||||
normalized_xp: stats.xp / stats.battles,
|
||||
normalized_battle_life_time: stats.battle_life_time / stats.battles,
|
||||
|
||||
cumulative_accuracy: stats.hits / stats.shots,
|
||||
cumulative_battles: stats.battles,
|
||||
cumulative_capture_points: stats.capture_points,
|
||||
cumulative_damage_dealt: stats.damage_dealt,
|
||||
cumulative_damage_ratio: stats.damage_dealt / stats.damage_received,
|
||||
cumulative_damage_received: stats.damage_received,
|
||||
cumulative_deaths: stats.battles - stats.survived_battles,
|
||||
cumulative_dropped_capture_points: stats.dropped_capture_points,
|
||||
cumulative_frags: stats.frags,
|
||||
cumulative_hits: stats.hits,
|
||||
cumulative_inaccuracy: (stats.shots - stats.hits) / stats.shots,
|
||||
cumulative_kills_to_death_ratio:
|
||||
stats.frags / (stats.battles - stats.survived_battles),
|
||||
cumulative_losses: stats.losses,
|
||||
cumulative_misses: stats.shots - stats.hits,
|
||||
cumulative_shots: stats.shots,
|
||||
cumulative_spotted: stats.spotted,
|
||||
cumulative_survived_battles: stats.survived_battles,
|
||||
cumulative_win_and_survived: stats.win_and_survived,
|
||||
cumulative_wins: stats.wins,
|
||||
cumulative_xp: stats.xp,
|
||||
cumulative_wn8: calculateWN8(average, stats),
|
||||
cumulative_battle_life_time: stats.battle_life_time,
|
||||
} satisfies CompositeStats;
|
||||
}
|
||||
|
||||
export function formatCompositeStat(value: number, key: CompositeStatsKey) {
|
||||
const formatting = compositeStatsFormatting[key];
|
||||
const resolvedValue = (formatting.coefficient ?? 1) * value;
|
||||
return `${
|
||||
formatting.localeFormat
|
||||
? resolvedValue.toLocaleString()
|
||||
: resolvedValue.toFixed(formatting.fixed)
|
||||
}${formatting.unit ?? ''}`;
|
||||
}
|
||||
|
||||
export function previewCompositeStat(key: CompositeStatsKey) {
|
||||
const { preview } = compositeStatsFormatting[key];
|
||||
return formatCompositeStat(preview, key);
|
||||
}
|
||||
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { RatingPlayer } from '../../commands/ratingLeaderboard';
|
||||
import { RatingStats } from '../blitz/getAccountInfo';
|
||||
import { BkrlSuperset1Entry } from '../streams/bkrl';
|
||||
|
||||
export function deltaBkrlBlitzStats(
|
||||
a: BkrlSuperset1Entry,
|
||||
b1: RatingStats,
|
||||
b2: RatingPlayer,
|
||||
) {
|
||||
return {
|
||||
battles: b1.battles - a.battles,
|
||||
damage: b1.damage_dealt - a.damage,
|
||||
kills: b1.frags - a.kills,
|
||||
survived: b1.survived_battles - a.survived,
|
||||
wins: b1.wins - a.wins,
|
||||
score: b2.score - a.score,
|
||||
} satisfies Omit<BkrlSuperset1Entry, 'id'>;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export function deltaTankStats(
|
||||
tanksA: IndividualTankStats[],
|
||||
tanksB: IndividualTankStats[],
|
||||
) {
|
||||
return tanksB
|
||||
.map((b) => {
|
||||
const a =
|
||||
tanksA.find((a) => a.tank_id === b.tank_id) ?? emptyIndividualTankStats;
|
||||
|
||||
return {
|
||||
account_id: b.account_id,
|
||||
battle_life_time: b.battle_life_time - a.battle_life_time,
|
||||
frags: b.frags === null || a.frags === null ? null : b.frags - a.frags,
|
||||
in_garage: b.in_garage,
|
||||
in_garage_updated: b.in_garage_updated,
|
||||
last_battle_time: b.last_battle_time,
|
||||
mark_of_mastery: b.mark_of_mastery,
|
||||
max_frags: b.max_frags,
|
||||
max_xp: b.max_xp,
|
||||
tank_id: b.tank_id,
|
||||
|
||||
all: {
|
||||
battles: b.all.battles - a.all.battles,
|
||||
capture_points: b.all.capture_points - a.all.capture_points,
|
||||
damage_dealt: b.all.damage_dealt - a.all.damage_dealt,
|
||||
damage_received: b.all.damage_received - a.all.damage_received,
|
||||
dropped_capture_points:
|
||||
b.all.dropped_capture_points - a.all.dropped_capture_points,
|
||||
frags: b.all.frags - a.all.frags,
|
||||
frags8p: b.all.frags8p - a.all.frags8p,
|
||||
hits: b.all.hits - a.all.hits,
|
||||
losses: b.all.losses - a.all.losses,
|
||||
max_frags: b.all.max_frags,
|
||||
max_xp: b.all.max_xp,
|
||||
shots: b.all.shots - a.all.shots,
|
||||
spotted: b.all.spotted - a.all.spotted,
|
||||
survived_battles: b.all.survived_battles - a.all.survived_battles,
|
||||
xp: b.all.xp - a.all.xp,
|
||||
win_and_survived: b.all.win_and_survived - a.all.win_and_survived,
|
||||
wins: b.all.wins - a.all.wins,
|
||||
},
|
||||
} satisfies IndividualTankStats;
|
||||
})
|
||||
.filter((a) => a !== null && a.all.battles > 0) as IndividualTankStats[];
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { tankDefinitions } from '../blitzkit/tankDefinitions';
|
||||
import calculateWN8 from './calculateWN8';
|
||||
import sumStats from './sumStats';
|
||||
|
||||
export interface StatFilters {
|
||||
nation?: string;
|
||||
tier?: number;
|
||||
tankType?: string;
|
||||
treeType?: TreeType;
|
||||
tank?: number;
|
||||
}
|
||||
|
||||
export async function filterStats(
|
||||
{ diff, order }: DiffedTankStats,
|
||||
filters: StatFilters,
|
||||
) {
|
||||
// tank, if provided, takes priority over all other filters
|
||||
if (filters.tank) filters = { tank: filters.tank };
|
||||
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const awaitedTankAverages = await tankAverages;
|
||||
const filteredOrder = order.filter((id) => {
|
||||
const entry = awaitedTankDefinitions[id];
|
||||
|
||||
return (
|
||||
entry &&
|
||||
(filters.nation === undefined || entry.nation === filters.nation) &&
|
||||
(filters.tier === undefined || entry.tier === filters.tier) &&
|
||||
(filters.tankType === undefined || entry.class === filters.tankType) &&
|
||||
(filters.treeType === undefined ||
|
||||
(filters.treeType === 'collector' && entry.treeType === 'collector') ||
|
||||
(filters.treeType === 'premium' && entry.treeType === 'premium') ||
|
||||
(filters.treeType === 'researchable' &&
|
||||
entry.treeType === 'researchable')) &&
|
||||
(filters.tank === undefined || entry.id === filters.tank)
|
||||
);
|
||||
});
|
||||
const stats = sumStats(filteredOrder.map((id) => diff[id]));
|
||||
const battlesOfTanksWithAverages = filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankAverages[id] ? accumulator + diff[id].battles : accumulator,
|
||||
0,
|
||||
);
|
||||
const battlesOfTanksWithTankDefinition = filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankDefinitions[id] ? accumulator + diff[id].battles : accumulator,
|
||||
0,
|
||||
);
|
||||
const supplementary = {
|
||||
WN8:
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankAverages[id]
|
||||
? accumulator +
|
||||
calculateWN8(awaitedTankAverages[id].all, diff[id]) *
|
||||
diff[id].battles
|
||||
: accumulator,
|
||||
0,
|
||||
) / battlesOfTanksWithAverages,
|
||||
tier:
|
||||
filteredOrder.reduce<number>(
|
||||
(accumulator, id) =>
|
||||
awaitedTankDefinitions[id]
|
||||
? accumulator + awaitedTankDefinitions[id]!.tier * diff[id].battles
|
||||
: accumulator,
|
||||
0,
|
||||
) / battlesOfTanksWithTankDefinition,
|
||||
} satisfies SupplementaryStats;
|
||||
|
||||
return { stats, supplementary, filteredOrder };
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export enum BkniPercentile {
|
||||
SuperMalum,
|
||||
Malum,
|
||||
VeryBad,
|
||||
Bad,
|
||||
BelowAverage,
|
||||
Average,
|
||||
AboveAverage,
|
||||
Good,
|
||||
VeryGood,
|
||||
Unicum,
|
||||
SuperUnicum,
|
||||
}
|
||||
|
||||
export const BKNI_PERCENTILES = [
|
||||
{ min: 0, percentile: BkniPercentile.SuperMalum },
|
||||
{ min: 1, percentile: BkniPercentile.Malum },
|
||||
{ min: 10, percentile: BkniPercentile.VeryBad },
|
||||
{ min: 20, percentile: BkniPercentile.Bad },
|
||||
{ min: 30, percentile: BkniPercentile.BelowAverage },
|
||||
{ min: 40, percentile: BkniPercentile.Average },
|
||||
{ min: 60, percentile: BkniPercentile.AboveAverage },
|
||||
{ min: 70, percentile: BkniPercentile.Good },
|
||||
{ min: 80, percentile: BkniPercentile.VeryGood },
|
||||
{ min: 90, percentile: BkniPercentile.Unicum },
|
||||
{ min: 99, percentile: BkniPercentile.SuperUnicum },
|
||||
];
|
||||
|
||||
export const BKNI_COLORS = {
|
||||
[BkniPercentile.SuperMalum]: 'tomato',
|
||||
[BkniPercentile.Malum]: 'orange',
|
||||
[BkniPercentile.VeryBad]: 'amber',
|
||||
[BkniPercentile.Bad]: 'yellow',
|
||||
[BkniPercentile.BelowAverage]: 'lime',
|
||||
[BkniPercentile.Average]: 'green',
|
||||
[BkniPercentile.AboveAverage]: 'teal',
|
||||
[BkniPercentile.Good]: 'cyan',
|
||||
[BkniPercentile.VeryGood]: 'pink',
|
||||
[BkniPercentile.Unicum]: 'plum',
|
||||
[BkniPercentile.SuperUnicum]: 'purple',
|
||||
} as const;
|
||||
|
||||
export default function getBkniPercentile(wn8: number) {
|
||||
const lastIndex = BKNI_PERCENTILES.findLastIndex(({ min }) => min <= wn8);
|
||||
return BKNI_PERCENTILES[lastIndex === -1 ? 0 : lastIndex].percentile;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TierWeightsRecord } from '../../components/TierWeights';
|
||||
import { tankDefinitions } from '../blitzkit/tankDefinitions';
|
||||
import { BlitzStats } from './compositeStats/constants';
|
||||
|
||||
export async function getTierWeights(
|
||||
stats: Record<number, BlitzStats>,
|
||||
order: number[] = Object.keys(stats).map(parseInt),
|
||||
) {
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const tierWeights: TierWeightsRecord = {};
|
||||
|
||||
order.forEach((id) => {
|
||||
const entry = awaitedTankDefinitions[id];
|
||||
|
||||
if (!entry) return;
|
||||
if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0;
|
||||
|
||||
tierWeights[entry.tier]! += stats[id].battles;
|
||||
});
|
||||
|
||||
return tierWeights;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Percentile } from './percentiles';
|
||||
|
||||
const WN8_PERCENTILES: [number, Percentile][] = [
|
||||
[0, Percentile.VeryBad],
|
||||
[300, Percentile.Bad],
|
||||
[450, Percentile.BelowAverage],
|
||||
[650, Percentile.Average],
|
||||
[900, Percentile.AboveAverage],
|
||||
[1200, Percentile.Good],
|
||||
[1600, Percentile.VeryGood],
|
||||
[2000, Percentile.Great],
|
||||
[2450, Percentile.Unicum],
|
||||
[2900, Percentile.SuperUnicum],
|
||||
];
|
||||
|
||||
export default function getWN8Percentile(WN8: number) {
|
||||
const lastIndex = WN8_PERCENTILES.findLastIndex(
|
||||
([WN8Listing]) => WN8Listing <= WN8,
|
||||
);
|
||||
return WN8_PERCENTILES[lastIndex === -1 ? 0 : lastIndex][1];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './calculateWN8';
|
||||
export * from './compositeStats';
|
||||
export * from './deltaBkrlBlitzStats';
|
||||
export * from './deltaTankStats';
|
||||
export * from './filterStats';
|
||||
export * from './getBkniPercentile';
|
||||
export * from './getTierWeights';
|
||||
export * from './getWN8Percentile';
|
||||
export * from './percentiles';
|
||||
export * from './sumStats';
|
||||
@@ -0,0 +1,13 @@
|
||||
// http://www.wotinfo.net/en/wn8scale
|
||||
export enum Percentile {
|
||||
VeryBad,
|
||||
Bad,
|
||||
BelowAverage,
|
||||
Average,
|
||||
AboveAverage,
|
||||
Good,
|
||||
VeryGood,
|
||||
Great,
|
||||
Unicum,
|
||||
SuperUnicum,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BlitzStats } from './compositeStats/constants';
|
||||
|
||||
export default function sumStats(allStatsCollection: BlitzStats[]) {
|
||||
return allStatsCollection.reduce((accumulated, current) => {
|
||||
function sum(value: (allStats: BlitzStats) => number) {
|
||||
return value(accumulated) + value(current);
|
||||
}
|
||||
function max(value: (allStats: BlitzStats) => number) {
|
||||
return Math.max(value(accumulated), value(current));
|
||||
}
|
||||
|
||||
return {
|
||||
battles: sum((a) => a.battles),
|
||||
wins: sum((a) => a.wins),
|
||||
capture_points: sum((a) => a.capture_points),
|
||||
damage_dealt: sum((a) => a.damage_dealt),
|
||||
damage_received: sum((a) => a.damage_received),
|
||||
dropped_capture_points: sum((a) => a.dropped_capture_points),
|
||||
frags: sum((a) => a.frags),
|
||||
frags8p: sum((a) => a.frags8p),
|
||||
hits: sum((a) => a.hits),
|
||||
losses: sum((a) => a.losses),
|
||||
max_frags: max((a) => a.max_frags),
|
||||
max_xp: max((a) => a.max_xp),
|
||||
shots: sum((a) => a.shots),
|
||||
spotted: sum((a) => a.spotted),
|
||||
survived_battles: sum((a) => a.survived_battles),
|
||||
win_and_survived: sum((a) => a.win_and_survived),
|
||||
xp: sum((a) => a.xp),
|
||||
} satisfies BlitzStats;
|
||||
}, emptyAllStats);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReadStream, WriteStream } from './buffer';
|
||||
|
||||
export enum BkrlFormat {
|
||||
Base,
|
||||
Superset1,
|
||||
}
|
||||
|
||||
export interface BkrlBaseEntry {
|
||||
id: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface BkrlSuperset1Entry {
|
||||
id: number;
|
||||
score: number;
|
||||
|
||||
battles: number;
|
||||
wins: number;
|
||||
survived: number;
|
||||
|
||||
damage: number;
|
||||
kills: number;
|
||||
}
|
||||
|
||||
export type BkrlDiscriminatedEntries =
|
||||
| {
|
||||
format: BkrlFormat.Base;
|
||||
entries: BkrlBaseEntry[];
|
||||
}
|
||||
| {
|
||||
format: BkrlFormat.Superset1;
|
||||
entries: BkrlSuperset1Entry[];
|
||||
};
|
||||
|
||||
export class BkrlReadStream extends ReadStream {
|
||||
bkrl() {
|
||||
this.magic();
|
||||
const header = this.header();
|
||||
return this.body(header);
|
||||
}
|
||||
|
||||
body(header: ReturnType<typeof this.header>): BkrlDiscriminatedEntries {
|
||||
switch (header.format) {
|
||||
case BkrlFormat.Base: {
|
||||
return {
|
||||
format: BkrlFormat.Base,
|
||||
entries: times(
|
||||
header.count,
|
||||
() =>
|
||||
({
|
||||
id: this.uint32(),
|
||||
score: this.uint16(),
|
||||
}) satisfies BkrlBaseEntry,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case BkrlFormat.Superset1: {
|
||||
return {
|
||||
format: BkrlFormat.Superset1,
|
||||
entries: times(
|
||||
header.count,
|
||||
() =>
|
||||
({
|
||||
id: this.uint32(),
|
||||
score: this.uint16(),
|
||||
|
||||
battles: this.uint32(),
|
||||
wins: this.uint32(),
|
||||
survived: this.uint32(),
|
||||
|
||||
damage: this.uint32(),
|
||||
kills: this.uint32(),
|
||||
}) satisfies BkrlSuperset1Entry,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
return {
|
||||
version: this.uint16(),
|
||||
format: this.uint8() as BkrlFormat,
|
||||
count: this.uint32(),
|
||||
};
|
||||
}
|
||||
|
||||
magic() {
|
||||
const magic = this.utf8(4);
|
||||
|
||||
if (magic !== 'BKRL') {
|
||||
throw new Error(`Invalid BKRL magic number: "${magic}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BkrlWriteStream extends WriteStream {
|
||||
bkrl(discriminatedEntries: BkrlDiscriminatedEntries) {
|
||||
this.magic();
|
||||
this.header(discriminatedEntries);
|
||||
this.body(discriminatedEntries);
|
||||
}
|
||||
|
||||
body(discriminatedEntries: BkrlDiscriminatedEntries) {
|
||||
switch (discriminatedEntries.format) {
|
||||
case BkrlFormat.Base: {
|
||||
discriminatedEntries.entries.forEach((entry) => {
|
||||
this.uint32(entry.id);
|
||||
this.uint16(entry.score);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case BkrlFormat.Superset1: {
|
||||
discriminatedEntries.entries.forEach((entry) => {
|
||||
this.uint32(entry.id);
|
||||
this.uint16(entry.score);
|
||||
|
||||
this.uint32(entry.battles);
|
||||
this.uint32(entry.wins);
|
||||
this.uint32(entry.survived);
|
||||
|
||||
this.uint32(entry.damage);
|
||||
this.uint32(entry.kills);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header(discriminatedEntries: BkrlDiscriminatedEntries) {
|
||||
this.uint16(1);
|
||||
this.uint8(discriminatedEntries.format);
|
||||
this.uint32(discriminatedEntries.entries.length);
|
||||
}
|
||||
|
||||
magic() {
|
||||
this.utf8('BKRL');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
export class ReadStream {
|
||||
public index = 0;
|
||||
private dataView: DataView;
|
||||
private textDecoderAscii = new TextDecoder('ascii');
|
||||
private textDecoderUtf8 = new TextDecoder('utf8');
|
||||
|
||||
constructor(public buffer: ArrayBuffer) {
|
||||
this.dataView = new DataView(buffer);
|
||||
}
|
||||
|
||||
seek(size: number) {
|
||||
this.index += size;
|
||||
}
|
||||
read(size: number) {
|
||||
return this.buffer.slice(this.index, this.index + size);
|
||||
}
|
||||
consume(size: number) {
|
||||
const subarray = this.read(size);
|
||||
this.seek(size);
|
||||
return subarray;
|
||||
}
|
||||
consumeUint8Array(size: number) {
|
||||
return new Uint8Array(this.consume(size));
|
||||
}
|
||||
increment(size: number) {
|
||||
return (this.index += size) - size;
|
||||
}
|
||||
|
||||
readRemaining() {
|
||||
return this.read(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
consumeRemaining() {
|
||||
return this.consume(Number.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
byte(size: number) {
|
||||
return this.consume(size);
|
||||
}
|
||||
|
||||
ascii(size: number) {
|
||||
return this.textDecoderAscii.decode(this.consume(size));
|
||||
}
|
||||
utf8(size: number) {
|
||||
return this.textDecoderUtf8.decode(this.consume(size));
|
||||
}
|
||||
|
||||
int8() {
|
||||
return this.dataView.getInt8(this.increment(1));
|
||||
}
|
||||
int16() {
|
||||
return this.dataView.getInt16(this.increment(2), true);
|
||||
}
|
||||
int32() {
|
||||
return this.dataView.getInt32(this.increment(4), true);
|
||||
}
|
||||
int64() {
|
||||
return this.dataView.getBigInt64(this.increment(8), true);
|
||||
}
|
||||
uint8() {
|
||||
return this.dataView.getUint8(this.increment(1));
|
||||
}
|
||||
uint16() {
|
||||
return this.dataView.getUint16(this.increment(2), true);
|
||||
}
|
||||
uint32() {
|
||||
return this.dataView.getUint32(this.increment(4), true);
|
||||
}
|
||||
uint64() {
|
||||
return this.dataView.getBigUint64(this.increment(8), true);
|
||||
}
|
||||
float32() {
|
||||
return this.dataView.getFloat32(this.increment(4), true);
|
||||
}
|
||||
float64() {
|
||||
return this.dataView.getFloat64(this.increment(8), true);
|
||||
}
|
||||
|
||||
boolean() {
|
||||
return this.dataView.getUint8(this.increment(1)) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class WriteStream {
|
||||
private array: number[] = [];
|
||||
private textEncoder = new TextEncoder();
|
||||
|
||||
get uint8Array() {
|
||||
return new Uint8Array(this.array);
|
||||
}
|
||||
|
||||
byte(value: Uint8Array) {
|
||||
this.array.push(...value);
|
||||
return this;
|
||||
}
|
||||
|
||||
ascii(value: string) {
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
this.array.push(value.charCodeAt(index));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
utf8(value: string) {
|
||||
this.array.push(...this.textEncoder.encode(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
private uintN(bytes: number, value: number) {
|
||||
for (let index = 0; index < bytes; index++) {
|
||||
this.array.push((value >>> (8 * index)) & 0xff);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
uint8(value: number) {
|
||||
this.uintN(1, value);
|
||||
return this;
|
||||
}
|
||||
uint16(value: number) {
|
||||
this.uintN(2, value);
|
||||
return this;
|
||||
}
|
||||
uint32(value: number) {
|
||||
this.uintN(4, value);
|
||||
return this;
|
||||
}
|
||||
uint64(value: bigint) {
|
||||
for (let index = 0n; index < 8n; index++) {
|
||||
this.uint8(Number((value >> (8n * index)) & 0xffn));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
private intN(bytes: number, value: number) {
|
||||
for (let index = 0; index < bytes; index++) {
|
||||
this.array.push((value >> (8 * index)) & 0xff);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
int8(value: number) {
|
||||
this.intN(1, value);
|
||||
return this;
|
||||
}
|
||||
int16(value: number) {
|
||||
this.intN(2, value);
|
||||
return this;
|
||||
}
|
||||
int32(value: number) {
|
||||
this.intN(4, value);
|
||||
return this;
|
||||
}
|
||||
int64(value: bigint) {
|
||||
for (let index = 0n; index < 8n; index++) {
|
||||
this.int8(Number((value >> (8n * index)) & 0xffn));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
float32(value: number) {
|
||||
const array = new Float32Array([value]);
|
||||
this.array.push(...new Uint8Array(array.buffer));
|
||||
return this;
|
||||
}
|
||||
float64(value: number) {
|
||||
const array = new Float64Array([value]);
|
||||
this.array.push(...new Uint8Array(array.buffer));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReadStream, WriteStream } from './buffer';
|
||||
|
||||
enum ValueType {
|
||||
Null,
|
||||
Boolean,
|
||||
Uint8,
|
||||
Uint16,
|
||||
Uint32,
|
||||
Uint64,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Float32,
|
||||
Float64,
|
||||
String,
|
||||
FastString,
|
||||
Array,
|
||||
Object,
|
||||
}
|
||||
|
||||
enum FastStringFormat {
|
||||
Format8,
|
||||
Format16,
|
||||
Format32,
|
||||
}
|
||||
|
||||
export type CdonValue =
|
||||
| undefined
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| bigint
|
||||
| string
|
||||
| CdonValue[]
|
||||
| { [key: string]: CdonValue };
|
||||
|
||||
export class CdonReadStream extends ReadStream {
|
||||
cdon() {
|
||||
this.magic();
|
||||
const header = this.header();
|
||||
const body = this.body(header.fastStringFormat, header.stringTable);
|
||||
return body;
|
||||
}
|
||||
|
||||
magic() {
|
||||
const magic = this.utf8(4);
|
||||
|
||||
if (magic !== 'CDON') {
|
||||
throw new Error(`Invalid CDON magic number: "${magic}"`);
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
const version = this.uint16();
|
||||
const fastStringFormat = this.uint8() as FastStringFormat;
|
||||
|
||||
return {
|
||||
version,
|
||||
fastStringFormat,
|
||||
stringTable: this.stringTable(fastStringFormat),
|
||||
};
|
||||
}
|
||||
|
||||
body(
|
||||
fastStringFormat: FastStringFormat,
|
||||
stringTable: Record<number, string>,
|
||||
) {
|
||||
return this.value(fastStringFormat, stringTable);
|
||||
}
|
||||
|
||||
stringTable(fastStringFormat: FastStringFormat) {
|
||||
let count: number;
|
||||
|
||||
switch (fastStringFormat) {
|
||||
case FastStringFormat.Format8:
|
||||
count = this.uint8();
|
||||
break;
|
||||
|
||||
case FastStringFormat.Format16:
|
||||
count = this.uint16();
|
||||
break;
|
||||
|
||||
case FastStringFormat.Format32:
|
||||
count = this.uint32();
|
||||
break;
|
||||
}
|
||||
|
||||
const strings: string[] = [];
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
strings.push(this.string());
|
||||
}
|
||||
|
||||
const table: Record<number, string> = {};
|
||||
times(count, (index) => (table[index] = strings[index]));
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
string() {
|
||||
const length = this.uint32();
|
||||
const string = this.utf8(length);
|
||||
return string;
|
||||
}
|
||||
|
||||
value(
|
||||
fastStringFormat: FastStringFormat,
|
||||
stringTable: Record<number, string>,
|
||||
): CdonValue {
|
||||
const type = this.uint8() as ValueType;
|
||||
|
||||
switch (type) {
|
||||
case ValueType.Null:
|
||||
return null;
|
||||
|
||||
case ValueType.Boolean:
|
||||
return this.boolean();
|
||||
|
||||
case ValueType.Uint8:
|
||||
return this.uint8();
|
||||
|
||||
case ValueType.Uint16:
|
||||
return this.uint16();
|
||||
|
||||
case ValueType.Uint32:
|
||||
return this.uint32();
|
||||
|
||||
case ValueType.Uint64:
|
||||
return this.uint64();
|
||||
|
||||
case ValueType.Int8:
|
||||
return this.int8();
|
||||
|
||||
case ValueType.Int16:
|
||||
return this.int16();
|
||||
|
||||
case ValueType.Int32:
|
||||
return this.int32();
|
||||
|
||||
case ValueType.Int64:
|
||||
return this.int64();
|
||||
|
||||
case ValueType.Float32:
|
||||
return this.float32();
|
||||
|
||||
case ValueType.Float64:
|
||||
return this.float64();
|
||||
|
||||
case ValueType.String:
|
||||
return this.string();
|
||||
|
||||
case ValueType.FastString: {
|
||||
switch (fastStringFormat) {
|
||||
case FastStringFormat.Format8:
|
||||
return stringTable[this.uint8()];
|
||||
|
||||
case FastStringFormat.Format16:
|
||||
return stringTable[this.uint16()];
|
||||
|
||||
case FastStringFormat.Format32:
|
||||
return stringTable[this.uint32()];
|
||||
}
|
||||
}
|
||||
|
||||
case ValueType.Array:
|
||||
return times(this.uint32(), () =>
|
||||
this.value(fastStringFormat, stringTable),
|
||||
);
|
||||
|
||||
case ValueType.Object: {
|
||||
const count = this.uint32();
|
||||
const keys = times(
|
||||
count,
|
||||
() => this.value(fastStringFormat, stringTable) as string,
|
||||
);
|
||||
const values = times(count, () =>
|
||||
this.value(fastStringFormat, stringTable),
|
||||
);
|
||||
const object: Record<string, CdonValue> = {};
|
||||
|
||||
keys.forEach((string, index) => (object[string] = values[index]));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid value type: ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CdonWriteStream extends WriteStream {
|
||||
cdon(object: CdonValue) {
|
||||
this.magic();
|
||||
const header = this.header(object);
|
||||
this.body(header.fastStringFormat, object, header.stringTable);
|
||||
return this;
|
||||
}
|
||||
|
||||
body(
|
||||
fastStringFormat: FastStringFormat,
|
||||
object: CdonValue,
|
||||
stringTable: Map<string, number>,
|
||||
) {
|
||||
this.value(fastStringFormat, object, stringTable);
|
||||
return this;
|
||||
}
|
||||
|
||||
value(
|
||||
fastStringFormat: FastStringFormat,
|
||||
object: CdonValue,
|
||||
stringTable: Map<string, number>,
|
||||
) {
|
||||
if (object === null || object === undefined) {
|
||||
this.uint8(ValueType.Null);
|
||||
} else if (typeof object === 'boolean') {
|
||||
this.uint8(ValueType.Boolean);
|
||||
this.uint8(object ? 1 : 0);
|
||||
} else if (typeof object === 'number') {
|
||||
if (object % 1 === 0) {
|
||||
if (object < 0) {
|
||||
if (object < -0x80000000) {
|
||||
this.uint8(ValueType.Int64);
|
||||
this.int64(BigInt(object));
|
||||
} else if (object < -0x8000) {
|
||||
this.uint8(ValueType.Int32);
|
||||
this.int32(object);
|
||||
} else if (object < -0x80) {
|
||||
this.uint8(ValueType.Int16);
|
||||
this.int16(object);
|
||||
} else {
|
||||
this.uint8(ValueType.Int8);
|
||||
this.int8(object);
|
||||
}
|
||||
} else {
|
||||
if (object > 0x7fffffff) {
|
||||
this.uint8(ValueType.Uint64);
|
||||
this.uint64(BigInt(object));
|
||||
} else if (object > 0x7fff) {
|
||||
this.uint8(ValueType.Uint32);
|
||||
this.uint32(object);
|
||||
} else if (object > 0x7f) {
|
||||
this.uint8(ValueType.Uint16);
|
||||
this.uint16(object);
|
||||
} else {
|
||||
this.uint8(ValueType.Uint8);
|
||||
this.uint8(object);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (object < -0x80000000 || object > 0x7fffffff) {
|
||||
this.uint8(ValueType.Float64);
|
||||
this.float64(object);
|
||||
} else {
|
||||
this.uint8(ValueType.Float32);
|
||||
this.float32(object);
|
||||
}
|
||||
}
|
||||
} else if (typeof object === 'bigint') {
|
||||
if (object < 0) {
|
||||
this.int64(object);
|
||||
} else {
|
||||
this.uint64(object);
|
||||
}
|
||||
} else if (typeof object === 'string') {
|
||||
if (stringTable.has(object)) {
|
||||
this.uint8(ValueType.FastString);
|
||||
const id = stringTable.get(object)!;
|
||||
switch (fastStringFormat) {
|
||||
case FastStringFormat.Format8:
|
||||
this.uint8(id);
|
||||
break;
|
||||
|
||||
case FastStringFormat.Format16:
|
||||
this.uint16(id);
|
||||
break;
|
||||
|
||||
case FastStringFormat.Format32:
|
||||
this.uint32(id);
|
||||
}
|
||||
} else {
|
||||
this.uint8(ValueType.String);
|
||||
this.string(object);
|
||||
}
|
||||
} else if (Array.isArray(object)) {
|
||||
this.uint8(ValueType.Array);
|
||||
this.uint32(object.length);
|
||||
object.forEach((value) =>
|
||||
this.value(fastStringFormat, value, stringTable),
|
||||
);
|
||||
} else {
|
||||
const entries = Object.entries(object).filter(
|
||||
([, value]) => value !== undefined,
|
||||
);
|
||||
|
||||
this.uint8(ValueType.Object);
|
||||
this.uint32(entries.length);
|
||||
entries.forEach(([key]) =>
|
||||
this.value(fastStringFormat, key, stringTable),
|
||||
);
|
||||
entries.forEach(([, value]) =>
|
||||
this.value(fastStringFormat, value, stringTable),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
header(object: CdonValue) {
|
||||
this.uint16(1);
|
||||
return this.stringTable(object);
|
||||
}
|
||||
|
||||
stringTable(object: CdonValue) {
|
||||
const stringTable = this.buildStringTable(
|
||||
this.collectStringCounts(object, new Map()),
|
||||
);
|
||||
let fastStringFormat: FastStringFormat;
|
||||
|
||||
if (stringTable.size <= 0xff) {
|
||||
fastStringFormat = FastStringFormat.Format8;
|
||||
this.uint8(FastStringFormat.Format8);
|
||||
this.uint8(stringTable.size);
|
||||
} else if (stringTable.size <= 0xffff) {
|
||||
fastStringFormat = FastStringFormat.Format16;
|
||||
this.uint8(FastStringFormat.Format16);
|
||||
this.uint16(stringTable.size);
|
||||
} else {
|
||||
fastStringFormat = FastStringFormat.Format32;
|
||||
this.uint8(FastStringFormat.Format32);
|
||||
this.uint32(stringTable.size);
|
||||
}
|
||||
|
||||
stringTable.forEach((_index, string) => this.string(string));
|
||||
|
||||
return { fastStringFormat, stringTable };
|
||||
}
|
||||
|
||||
buildStringTable(stringCounts: Map<string, number>) {
|
||||
const stringTable = new Map<string, number>();
|
||||
|
||||
stringCounts.forEach((count, string) => {
|
||||
if (count > 1) stringTable.set(string, stringTable.size);
|
||||
});
|
||||
|
||||
return stringTable;
|
||||
}
|
||||
|
||||
collectStringCounts(object: CdonValue, stringCounts: Map<string, number>) {
|
||||
if (typeof object === 'string') {
|
||||
if (stringCounts.has(object)) {
|
||||
stringCounts.set(object, stringCounts.get(object)! + 1);
|
||||
} else {
|
||||
stringCounts.set(object, 1);
|
||||
}
|
||||
} else if (object instanceof Array) {
|
||||
object.forEach((value) => this.collectStringCounts(value, stringCounts));
|
||||
} else if (object instanceof Object) {
|
||||
Object.entries(object).forEach((strings) =>
|
||||
this.collectStringCounts(strings, stringCounts),
|
||||
);
|
||||
}
|
||||
|
||||
return stringCounts;
|
||||
}
|
||||
|
||||
string(value: string) {
|
||||
this.uint32(Buffer.byteLength(value));
|
||||
this.utf8(value);
|
||||
}
|
||||
|
||||
magic() {
|
||||
this.utf8('CDON');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
import { times } from 'lodash';
|
||||
import { Vector3Tuple, Vector4Tuple } from 'three';
|
||||
import { WindowsReadStream } from './windows';
|
||||
|
||||
enum DxgiFormat {
|
||||
UNKNOWN = 0,
|
||||
R32G32B32A32_TYPELESS = 1,
|
||||
R32G32B32A32_FLOAT = 2,
|
||||
R32G32B32A32_UINT = 3,
|
||||
R32G32B32A32_SINT = 4,
|
||||
R32G32B32_TYPELESS = 5,
|
||||
R32G32B32_FLOAT = 6,
|
||||
R32G32B32_UINT = 7,
|
||||
R32G32B32_SINT = 8,
|
||||
R16G16B16A16_TYPELESS = 9,
|
||||
R16G16B16A16_FLOAT = 10,
|
||||
R16G16B16A16_UNORM = 11,
|
||||
R16G16B16A16_UINT = 12,
|
||||
R16G16B16A16_SNORM = 13,
|
||||
R16G16B16A16_SINT = 14,
|
||||
R32G32_TYPELESS = 15,
|
||||
R32G32_FLOAT = 16,
|
||||
R32G32_UINT = 17,
|
||||
R32G32_SINT = 18,
|
||||
R32G8X24_TYPELESS = 19,
|
||||
D32_FLOAT_S8X24_UINT = 20,
|
||||
R32_FLOAT_X8X24_TYPELESS = 21,
|
||||
X32_TYPELESS_G8X24_UINT = 22,
|
||||
R10G10B10A2_TYPELESS = 23,
|
||||
R10G10B10A2_UNORM = 24,
|
||||
R10G10B10A2_UINT = 25,
|
||||
R11G11B10_FLOAT = 26,
|
||||
R8G8B8A8_TYPELESS = 27,
|
||||
R8G8B8A8_UNORM = 28,
|
||||
R8G8B8A8_UNORM_SRGB = 29,
|
||||
R8G8B8A8_UINT = 30,
|
||||
R8G8B8A8_SNORM = 31,
|
||||
R8G8B8A8_SINT = 32,
|
||||
R16G16_TYPELESS = 33,
|
||||
R16G16_FLOAT = 34,
|
||||
R16G16_UNORM = 35,
|
||||
R16G16_UINT = 36,
|
||||
R16G16_SNORM = 37,
|
||||
R16G16_SINT = 38,
|
||||
R32_TYPELESS = 39,
|
||||
D32_FLOAT = 40,
|
||||
R32_FLOAT = 41,
|
||||
R32_UINT = 42,
|
||||
R32_SINT = 43,
|
||||
R24G8_TYPELESS = 44,
|
||||
D24_UNORM_S8_UINT = 45,
|
||||
R24_UNORM_X8_TYPELESS = 46,
|
||||
X24_TYPELESS_G8_UINT = 47,
|
||||
R8G8_TYPELESS = 48,
|
||||
R8G8_UNORM = 49,
|
||||
R8G8_UINT = 50,
|
||||
R8G8_SNORM = 51,
|
||||
R8G8_SINT = 52,
|
||||
R16_TYPELESS = 53,
|
||||
R16_FLOAT = 54,
|
||||
D16_UNORM = 55,
|
||||
R16_UNORM = 56,
|
||||
R16_UINT = 57,
|
||||
R16_SNORM = 58,
|
||||
R16_SINT = 59,
|
||||
R8_TYPELESS = 60,
|
||||
R8_UNORM = 61,
|
||||
R8_UINT = 62,
|
||||
R8_SNORM = 63,
|
||||
R8_SINT = 64,
|
||||
A8_UNORM = 65,
|
||||
R1_UNORM = 66,
|
||||
R9G9B9E5_SHAREDEXP = 67,
|
||||
R8G8_B8G8_UNORM = 68,
|
||||
G8R8_G8B8_UNORM = 69,
|
||||
BC1_TYPELESS = 70,
|
||||
BC1_UNORM = 71,
|
||||
BC1_UNORM_SRGB = 72,
|
||||
BC2_TYPELESS = 73,
|
||||
BC2_UNORM = 74,
|
||||
BC2_UNORM_SRGB = 75,
|
||||
BC3_TYPELESS = 76,
|
||||
BC3_UNORM = 77,
|
||||
BC3_UNORM_SRGB = 78,
|
||||
BC4_TYPELESS = 79,
|
||||
BC4_UNORM = 80,
|
||||
BC4_SNORM = 81,
|
||||
BC5_TYPELESS = 82,
|
||||
BC5_UNORM = 83,
|
||||
BC5_SNORM = 84,
|
||||
B5G6R5_UNORM = 85,
|
||||
B5G5R5A1_UNORM = 86,
|
||||
B8G8R8A8_UNORM = 87,
|
||||
B8G8R8X8_UNORM = 88,
|
||||
R10G10B10_XR_BIAS_A2_UNORM = 89,
|
||||
B8G8R8A8_TYPELESS = 90,
|
||||
B8G8R8A8_UNORM_SRGB = 91,
|
||||
B8G8R8X8_TYPELESS = 92,
|
||||
B8G8R8X8_UNORM_SRGB = 93,
|
||||
BC6H_TYPELESS = 94,
|
||||
BC6H_UF16 = 95,
|
||||
BC6H_SF16 = 96,
|
||||
BC7_TYPELESS = 97,
|
||||
BC7_UNORM = 98,
|
||||
BC7_UNORM_SRGB = 99,
|
||||
AYUV = 100,
|
||||
Y410 = 101,
|
||||
Y416 = 102,
|
||||
NV12 = 103,
|
||||
P010 = 104,
|
||||
P016 = 105,
|
||||
'420_OPAQUE' = 106,
|
||||
YUY2 = 107,
|
||||
Y210 = 108,
|
||||
Y216 = 109,
|
||||
NV11 = 110,
|
||||
AI44 = 111,
|
||||
IA44 = 112,
|
||||
P8 = 113,
|
||||
A8P8 = 114,
|
||||
B4G4R4A4_UNORM = 115,
|
||||
P208 = 130,
|
||||
V208 = 131,
|
||||
V408 = 132,
|
||||
SAMPLER_FEEDBACK_MIN_MIP_OPAQUE,
|
||||
SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE,
|
||||
FORCE_UINT = 0xffffffff,
|
||||
}
|
||||
|
||||
enum DdsDimension {
|
||||
UNKNOWN = 0,
|
||||
BUFFER = 1,
|
||||
TEXTURE1D = 2,
|
||||
TEXTURE2D = 3,
|
||||
TEXTURE3D = 4,
|
||||
}
|
||||
|
||||
enum DdsdFlags {
|
||||
CAPS = 0x1,
|
||||
HEIGHT = 0x2,
|
||||
WIDTH = 0x4,
|
||||
PITCH = 0x8,
|
||||
PIXELFORMAT = 0x1000,
|
||||
MIPMAPCOUNT = 0x20000,
|
||||
LINEARSIZE = 0x80000,
|
||||
DEPTH = 0x800000,
|
||||
}
|
||||
|
||||
enum DdpfFlags {
|
||||
ALPHA = 0x1,
|
||||
FOURCC = 0x4,
|
||||
RGB = 0x40,
|
||||
YUV = 0x200,
|
||||
LUMINANCE = 0x20000,
|
||||
}
|
||||
|
||||
enum DdsCaps {
|
||||
COMPLEX = 0x8,
|
||||
MIPMAP = 0x400000,
|
||||
TEXTURE = 0x1000,
|
||||
}
|
||||
|
||||
enum DdsCaps2 {
|
||||
CUBEMAP = 0x200,
|
||||
CUBEMAP_POSITIVEX = 0x400,
|
||||
CUBEMAP_NEGATIVEX = 0x800,
|
||||
CUBEMAP_POSITIVEY = 0x1000,
|
||||
CUBEMAP_NEGATIVEY = 0x2000,
|
||||
CUBEMAP_POSITIVEZ = 0x4000,
|
||||
CUBEMAP_NEGATIVEZ = 0x8000,
|
||||
VOLUME = 0x200000,
|
||||
}
|
||||
|
||||
export class DdsReadStream extends WindowsReadStream {
|
||||
async dds() {
|
||||
this.magicNumber();
|
||||
const header = this.header();
|
||||
const dxgiFormat = this.resolveDxgiFormat(header);
|
||||
|
||||
switch (dxgiFormat) {
|
||||
case DxgiFormat.BC1_TYPELESS:
|
||||
case DxgiFormat.BC1_UNORM:
|
||||
case DxgiFormat.BC1_UNORM_SRGB: {
|
||||
return this.iterateBlocks(header.width, header.height, () => {
|
||||
const colorInterpolations = this.bc1ColorInterpolations();
|
||||
const colorKeys = this.bc1ColorKeys();
|
||||
return (index) => colorInterpolations[colorKeys[index]];
|
||||
});
|
||||
}
|
||||
|
||||
case DxgiFormat.BC2_TYPELESS:
|
||||
case DxgiFormat.BC2_UNORM:
|
||||
case DxgiFormat.BC2_UNORM_SRGB: {
|
||||
return this.iterateBlocks(header.width, header.height, () => {
|
||||
const alphaInterpolations = this.bc2AlphaInterpolations();
|
||||
const colorInterpolations = this.bc2ColorInterpolations();
|
||||
const colorKeys = this.bc1ColorKeys();
|
||||
return (index) => {
|
||||
return [
|
||||
...colorInterpolations[colorKeys[index]],
|
||||
alphaInterpolations[index],
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
case DxgiFormat.BC3_TYPELESS:
|
||||
case DxgiFormat.BC3_UNORM:
|
||||
case DxgiFormat.BC3_UNORM_SRGB: {
|
||||
return this.iterateBlocks(header.width, header.height, () => {
|
||||
const alphaInterpolations = this.bc3AlphaInterpolations();
|
||||
const alphaKeys = this.bc3AlphaKeys();
|
||||
const colorInterpolations = this.bc2ColorInterpolations();
|
||||
const colorKeys = this.bc1ColorKeys();
|
||||
return (index) => [
|
||||
...colorInterpolations[colorKeys[index]],
|
||||
alphaInterpolations[alphaKeys[index]],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Unsupported DXGI format: ${DxgiFormat[dxgiFormat]} (${dxgiFormat})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
magicNumber() {
|
||||
const magic = this.dword();
|
||||
|
||||
if (magic !== 0x20534444) {
|
||||
throw new Error(`Invalid DDS magic number: 0x${magic.toString(16)}`);
|
||||
}
|
||||
|
||||
return magic;
|
||||
}
|
||||
pixelFormat() {
|
||||
return {
|
||||
size: this.dword(),
|
||||
flags: this.dword() as DdpfFlags,
|
||||
fourCC: this.ascii(4),
|
||||
rgbBitCount: this.dword(),
|
||||
rBitMask: this.dword(),
|
||||
gBitMask: this.dword(),
|
||||
bBitMask: this.dword(),
|
||||
aBitMask: this.dword(),
|
||||
};
|
||||
}
|
||||
header() {
|
||||
return {
|
||||
size: this.dword(),
|
||||
flags: this.dword() as DdsdFlags,
|
||||
height: this.dword(),
|
||||
width: this.dword(),
|
||||
pitchOrLinearSize: this.dword(),
|
||||
depth: this.dword(),
|
||||
mipMapCount: this.dword(),
|
||||
reserved1: times(11, () => this.dword()),
|
||||
pf: this.pixelFormat(),
|
||||
caps: this.dword() as DdsCaps,
|
||||
caps2: this.dword() as DdsCaps2,
|
||||
caps3: this.dword(),
|
||||
caps4: this.dword(),
|
||||
reserved2: times(1, () => this.dword()),
|
||||
};
|
||||
}
|
||||
headerDxt10() {
|
||||
return {
|
||||
dxgiFormat: this.dword() as DxgiFormat,
|
||||
resourceDimension: this.dword() as DdsDimension,
|
||||
miscFlag: this.dword(),
|
||||
arraySize: this.dword(),
|
||||
miscFlags2: this.dword(),
|
||||
};
|
||||
}
|
||||
|
||||
resolveDxgiFormat(header: ReturnType<typeof this.header>) {
|
||||
switch (header.pf.fourCC) {
|
||||
case 'DXT1':
|
||||
return DxgiFormat.BC1_UNORM;
|
||||
|
||||
case 'DXT3':
|
||||
return DxgiFormat.BC2_UNORM;
|
||||
|
||||
case 'DXT5':
|
||||
return DxgiFormat.BC3_UNORM;
|
||||
|
||||
case 'DX10':
|
||||
return this.headerDxt10().dxgiFormat;
|
||||
|
||||
default:
|
||||
throw new TypeError(`Unsupported FourCC: ${header.pf.fourCC}`);
|
||||
}
|
||||
}
|
||||
|
||||
assetFactor4(width: number, height: number) {
|
||||
if (width % 4 !== 0 || height % 4 !== 0) {
|
||||
throw new RangeError('Width and height must be divisible by 4');
|
||||
}
|
||||
}
|
||||
iterateBlocks(
|
||||
width: number,
|
||||
height: number,
|
||||
blockIterator: () => (index: number) => Vector4Tuple,
|
||||
) {
|
||||
this.assetFactor4(width, height);
|
||||
const data = Buffer.alloc(width * height * 4);
|
||||
const blocksX = width / 4;
|
||||
const blocksY = height / 4;
|
||||
|
||||
times(blocksX * blocksY, (index) => {
|
||||
const blockPosX = (index % blocksX) * 4;
|
||||
const blockPosY = Math.floor(index / blocksX) * 4;
|
||||
const pixelIterator = blockIterator();
|
||||
|
||||
times(16, (pixelIndex) => {
|
||||
const pixelPosX = pixelIndex % 4;
|
||||
const pixelPosY = Math.floor(pixelIndex / 4);
|
||||
const x = blockPosX + pixelPosX;
|
||||
const y = blockPosY + pixelPosY;
|
||||
const bufferIndex = 4 * (y * width + x);
|
||||
|
||||
pixelIterator(pixelIndex).forEach((channel, index) => {
|
||||
data[bufferIndex + index] = Math.round(channel * 255);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { data, width, height, channels: 4 as const };
|
||||
}
|
||||
|
||||
readR5G6B5Int() {
|
||||
return new DataView(this.read(2)).getUint16(0, true);
|
||||
}
|
||||
R5G6B5() {
|
||||
const buffer = this.consumeUint8Array(2);
|
||||
const r = (buffer[1] & 0b11111000) >>> 3;
|
||||
const g = ((buffer[1] & 0b111) << 3) | ((buffer[0] & 0b11100000) >>> 5);
|
||||
const b = buffer[0] & 0b11111;
|
||||
|
||||
return [r / 31, g / 63, b / 31] as Vector3Tuple;
|
||||
}
|
||||
R5G6B5A0() {
|
||||
return [...this.R5G6B5(), 1] as Vector4Tuple;
|
||||
}
|
||||
A8() {
|
||||
return this.uint8() / 255;
|
||||
}
|
||||
|
||||
bc1ColorInterpolations() {
|
||||
const int0 = this.readR5G6B5Int();
|
||||
const color0 = this.R5G6B5A0();
|
||||
const int1 = this.readR5G6B5Int();
|
||||
const color1 = this.R5G6B5A0();
|
||||
const alpha = int0 < int1;
|
||||
const color2 = alpha
|
||||
? (color0.map(
|
||||
(channel0, index) => (channel0 + color1[index]) / 2,
|
||||
) as Vector4Tuple)
|
||||
: (color0.map(
|
||||
(channel0, index) => (2 * channel0 + color1[index]) / 3,
|
||||
) as Vector4Tuple);
|
||||
const color3 = alpha
|
||||
? ([0, 0, 0, 0] as Vector4Tuple)
|
||||
: (color0.map(
|
||||
(channel0, index) => (channel0 + 2 * color1[index]) / 3,
|
||||
) as Vector4Tuple);
|
||||
|
||||
return {
|
||||
[0b00]: color0,
|
||||
[0b01]: color1,
|
||||
[0b10]: color2,
|
||||
[0b11]: color3,
|
||||
} as Record<number, Vector4Tuple>;
|
||||
}
|
||||
bc1ColorKeys() {
|
||||
const buffer = this.consumeUint8Array(4);
|
||||
const d = (buffer[0] & 0b11000000) >>> 6;
|
||||
const c = (buffer[0] & 0b110000) >>> 4;
|
||||
const b = (buffer[0] & 0b1100) >>> 2;
|
||||
const a = buffer[0] & 0b11;
|
||||
const h = (buffer[1] & 0b11000000) >>> 6;
|
||||
const g = (buffer[1] & 0b110000) >>> 4;
|
||||
const f = (buffer[1] & 0b1100) >>> 2;
|
||||
const e = buffer[1] & 0b11;
|
||||
const l = (buffer[2] & 0b11000000) >>> 6;
|
||||
const k = (buffer[2] & 0b110000) >>> 4;
|
||||
const j = (buffer[2] & 0b1100) >>> 2;
|
||||
const i = buffer[2] & 0b11;
|
||||
const p = (buffer[3] & 0b11000000) >>> 6;
|
||||
const o = (buffer[3] & 0b110000) >>> 4;
|
||||
const n = (buffer[3] & 0b1100) >>> 2;
|
||||
const m = buffer[3] & 0b11;
|
||||
|
||||
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
|
||||
}
|
||||
bc2AlphaInterpolations() {
|
||||
const buffer = this.consumeUint8Array(8);
|
||||
const d = (buffer[1] & 0b11110000) >>> 4;
|
||||
const c = buffer[1] & 0b1111;
|
||||
const b = (buffer[0] & 0b11110000) >>> 4;
|
||||
const a = buffer[0] & 0b1111;
|
||||
const h = (buffer[3] & 0b11110000) >>> 4;
|
||||
const g = buffer[3] & 0b1111;
|
||||
const f = (buffer[2] & 0b11110000) >>> 4;
|
||||
const e = buffer[2] & 0b1111;
|
||||
const l = (buffer[5] & 0b11110000) >>> 4;
|
||||
const k = buffer[5] & 0b1111;
|
||||
const j = (buffer[4] & 0b11110000) >>> 4;
|
||||
const i = buffer[4] & 0b1111;
|
||||
const p = (buffer[7] & 0b11110000) >>> 4;
|
||||
const o = buffer[7] & 0b1111;
|
||||
const n = (buffer[6] & 0b11110000) >>> 4;
|
||||
const m = buffer[6] & 0b1111;
|
||||
|
||||
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p].map(
|
||||
(alpha) => alpha / 15,
|
||||
);
|
||||
}
|
||||
bc2ColorInterpolations() {
|
||||
const color0 = this.R5G6B5();
|
||||
const color1 = this.R5G6B5();
|
||||
const color2 = color0.map(
|
||||
(channel0, index) => (2 * channel0 + color1[index]) / 3,
|
||||
) as Vector3Tuple;
|
||||
const color3 = color0.map(
|
||||
(channel0, index) => (channel0 + 2 * color1[index]) / 3,
|
||||
) as Vector3Tuple;
|
||||
|
||||
return {
|
||||
[0b00]: color0,
|
||||
[0b01]: color1,
|
||||
[0b10]: color2,
|
||||
[0b11]: color3,
|
||||
} as Record<number, Vector3Tuple>;
|
||||
}
|
||||
bc3AlphaInterpolations() {
|
||||
const alpha0 = this.A8();
|
||||
const alpha1 = this.A8();
|
||||
const full = alpha0 > alpha1;
|
||||
|
||||
return {
|
||||
[0b000]: alpha0,
|
||||
[0b001]: alpha1,
|
||||
|
||||
...(full
|
||||
? {
|
||||
[0b010]: (6 * alpha0 + 1 * alpha1) / 7,
|
||||
[0b011]: (5 * alpha0 + 2 * alpha1) / 7,
|
||||
[0b100]: (4 * alpha0 + 3 * alpha1) / 7,
|
||||
[0b101]: (3 * alpha0 + 4 * alpha1) / 7,
|
||||
[0b110]: (2 * alpha0 + 5 * alpha1) / 7,
|
||||
[0b111]: (1 * alpha0 + 6 * alpha1) / 7,
|
||||
}
|
||||
: {
|
||||
[0b010]: (4 * alpha0 + 1 * alpha1) / 5,
|
||||
[0b011]: (3 * alpha0 + 2 * alpha1) / 5,
|
||||
[0b100]: (2 * alpha0 + 3 * alpha1) / 5,
|
||||
[0b101]: (1 * alpha0 + 4 * alpha1) / 5,
|
||||
[0b110]: 0,
|
||||
[0b111]: 255,
|
||||
}),
|
||||
} as Record<number, number>;
|
||||
}
|
||||
bc3AlphaKeys() {
|
||||
const buffer = this.consumeUint8Array(6);
|
||||
const h = (buffer[2] & 0b11100000) >>> 5;
|
||||
const g = (buffer[2] & 0b11100) >>> 2;
|
||||
const f = ((buffer[2] & 0b11) << 1) | ((buffer[1] & 0b10000000) >>> 7);
|
||||
const e = (buffer[1] & 0b1110000) >>> 4;
|
||||
const d = (buffer[1] & 0b1110) >>> 1;
|
||||
const c = ((buffer[1] & 0b1) << 2) | ((buffer[0] & 0b11000000) >>> 6);
|
||||
const b = (buffer[0] & 0b111000) >>> 3;
|
||||
const a = buffer[0] & 0b111;
|
||||
const p = (buffer[5] & 0b11100000) >>> 5;
|
||||
const o = (buffer[5] & 0b11100) >>> 2;
|
||||
const n = ((buffer[5] & 0b11) << 1) | ((buffer[4] & 0b10000000) >>> 7);
|
||||
const m = (buffer[4] & 0b1110000) >>> 4;
|
||||
const l = (buffer[4] & 0b1110) >>> 1;
|
||||
const k = ((buffer[4] & 0b1) << 2) | ((buffer[3] & 0b11000000) >>> 6);
|
||||
const j = (buffer[3] & 0b111000) >>> 3;
|
||||
const i = buffer[3] & 0b111;
|
||||
|
||||
return [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReadStream, WriteStream } from './buffer';
|
||||
|
||||
export class DidsReadStream extends ReadStream {
|
||||
dids() {
|
||||
this.magic();
|
||||
this.header();
|
||||
return this.body();
|
||||
}
|
||||
|
||||
magic() {
|
||||
const magic = this.utf8(4);
|
||||
if (magic !== 'DIDS') {
|
||||
throw new Error(`Invalid DIDS magic number: "${magic}"`);
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
return {
|
||||
version: this.uint16(),
|
||||
};
|
||||
}
|
||||
|
||||
body() {
|
||||
const count = this.uint32();
|
||||
return times(count, () => this.uint32());
|
||||
}
|
||||
}
|
||||
|
||||
export class DidsWriteStream extends WriteStream {
|
||||
dids(ids: number[]) {
|
||||
this.magic();
|
||||
this.header();
|
||||
this.body(ids);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
magic() {
|
||||
this.utf8('DIDS');
|
||||
}
|
||||
|
||||
header() {
|
||||
this.uint16(1);
|
||||
}
|
||||
|
||||
body(ids: number[]) {
|
||||
this.uint32(ids.length);
|
||||
ids.forEach((id) => this.uint32(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export * from './bkrl';
|
||||
export * from './buffer';
|
||||
export * from './cdon';
|
||||
export * from './dds';
|
||||
export * from './dids';
|
||||
export * from './pvr';
|
||||
export * from './rts';
|
||||
export * from './sc2';
|
||||
export * from './scg';
|
||||
export * from './scpg';
|
||||
export * from './windows';
|
||||
@@ -0,0 +1,239 @@
|
||||
import { clamp, isEqual, times } from 'lodash';
|
||||
import { ReadStream } from './buffer';
|
||||
|
||||
export enum PvrFlags {
|
||||
NoFlag = 0,
|
||||
PreMultiplied = 0x02,
|
||||
}
|
||||
|
||||
export enum PvrPixelFormat {
|
||||
PVRTC_2bpp_RGB = 0,
|
||||
PVRTC_2bpp_RGBA = 1,
|
||||
PVRTC_4bpp_RGB = 2,
|
||||
PVRTC_4bpp_RGBA = 3,
|
||||
PVRTC_II_2bpp = 4,
|
||||
PVRTC_II_4bpp = 5,
|
||||
ETC1 = 6,
|
||||
DXT1_BC1 = 7,
|
||||
DXT2 = 8,
|
||||
DXT3_BC2 = 9,
|
||||
DXT4 = 10,
|
||||
DXT5_BC3 = 11,
|
||||
BC4 = 12,
|
||||
BC5 = 13,
|
||||
BC6 = 14,
|
||||
BC7 = 15,
|
||||
UYVY = 16,
|
||||
YUY2 = 17,
|
||||
BW1bpp = 18,
|
||||
R9G9B9E5_SharedExponent = 19,
|
||||
RGBG8888 = 20,
|
||||
GRGB8888 = 21,
|
||||
ETC2_RGB = 22,
|
||||
ETC2_RGBA = 23,
|
||||
ETC2_RGB_A1 = 24,
|
||||
EAC_R11 = 25,
|
||||
EAC_RG11 = 26,
|
||||
ASTC_4x4 = 27,
|
||||
ASTC_5x4 = 28,
|
||||
ASTC_5x5 = 29,
|
||||
ASTC_6x5 = 30,
|
||||
ASTC_6x6 = 31,
|
||||
ASTC_8x5 = 32,
|
||||
ASTC_8x6 = 33,
|
||||
ASTC_8x8 = 34,
|
||||
ASTC_10x5 = 35,
|
||||
ASTC_10x6 = 36,
|
||||
ASTC_10x8 = 37,
|
||||
ASTC_10x10 = 38,
|
||||
ASTC_12x10 = 39,
|
||||
ASTC_12x12 = 40,
|
||||
ASTC_3x3x3 = 41,
|
||||
ASTC_4x3x3 = 42,
|
||||
ASTC_4x4x3 = 43,
|
||||
ASTC_4x4x4 = 44,
|
||||
ASTC_5x4x4 = 45,
|
||||
ASTC_5x5x4 = 46,
|
||||
ASTC_5x5x5 = 47,
|
||||
ASTC_6x5x5 = 48,
|
||||
ASTC_6x6x5 = 49,
|
||||
ASTC_6x6x6 = 50,
|
||||
BASISU_ETC1S = 51,
|
||||
BASISU_UASTC = 52,
|
||||
RGBM = 53,
|
||||
RGBD = 54,
|
||||
}
|
||||
|
||||
enum PvrColorSpace {
|
||||
LinearRGB = 0,
|
||||
sRGB = 1,
|
||||
}
|
||||
|
||||
enum PvrChannelType {
|
||||
UnsignedByteNormalised = 0,
|
||||
SignedByteNormalised = 1,
|
||||
UnsignedByte = 2,
|
||||
SignedByte = 3,
|
||||
UnsignedShortNormalised = 4,
|
||||
SignedShortNormalised = 5,
|
||||
UnsignedShort = 6,
|
||||
SignedShort = 7,
|
||||
UnsignedIntegerNormalised = 8,
|
||||
SignedIntegerNormalised = 9,
|
||||
UnsignedInteger = 10,
|
||||
SignedInteger = 11,
|
||||
SignedFloat = 12,
|
||||
UnsignedFloat = 13,
|
||||
}
|
||||
|
||||
enum ResolvedBitRatePixelFormat {
|
||||
R4G4B4A4,
|
||||
R5G6B5A0,
|
||||
}
|
||||
|
||||
export class PvrReadStream extends ReadStream {
|
||||
pvr() {
|
||||
const header = this.header();
|
||||
this.metadata();
|
||||
|
||||
if (typeof header.pixelFormat === 'number') {
|
||||
switch (header.pixelFormat) {
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Unhandled pixel format ${PvrPixelFormat[header.pixelFormat]} (${
|
||||
header.pixelFormat
|
||||
})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const resolvedPixelFormat = this.resolveBitRatePixelFormat(
|
||||
header.pixelFormat,
|
||||
);
|
||||
const pixelCount = header.width * header.height;
|
||||
|
||||
switch (resolvedPixelFormat) {
|
||||
case ResolvedBitRatePixelFormat.R4G4B4A4: {
|
||||
const data = Buffer.alloc(pixelCount * 4);
|
||||
|
||||
times(pixelCount, (index) => {
|
||||
const buffer = this.consumeUint8Array(2);
|
||||
const bufferIndex = index * 4;
|
||||
const r = ((buffer[1] & 0b11110000) >>> 4) / 15;
|
||||
const g = (buffer[1] & 0b1111) / 15;
|
||||
const b = ((buffer[0] & 0b11110000) >>> 4) / 15;
|
||||
const a = (buffer[0] & 0b1111) / 15;
|
||||
|
||||
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
|
||||
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
|
||||
data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255));
|
||||
data[bufferIndex + 3] = Math.round(clamp(a * 255, 0, 255));
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
width: header.width,
|
||||
height: header.height,
|
||||
channels: 4 as const,
|
||||
};
|
||||
}
|
||||
|
||||
case ResolvedBitRatePixelFormat.R5G6B5A0: {
|
||||
const data = Buffer.alloc(pixelCount * 3);
|
||||
|
||||
times(pixelCount, (index) => {
|
||||
const buffer = this.consumeUint8Array(2);
|
||||
const bufferIndex = index * 4;
|
||||
const r = ((buffer[1] & 0b11111000) >>> 3) / 31;
|
||||
const g =
|
||||
((buffer[1] & 0b111) << 3) |
|
||||
(((buffer[0] & 0b11100000) >>> 5) / 31);
|
||||
const b = buffer[0] & 0b11111;
|
||||
|
||||
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
|
||||
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
|
||||
data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255));
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
width: header.width,
|
||||
height: header.height,
|
||||
channels: 3 as const,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Unhandled resolved pixel format ${resolvedPixelFormat} (${header.pixelFormat})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
const header = {
|
||||
version: this.ascii(4),
|
||||
flags: this.uint32() as PvrFlags,
|
||||
pixelFormat: this.pixelFormat(),
|
||||
colorSpace: this.uint32() as PvrColorSpace,
|
||||
channelType: this.uint32() as PvrChannelType,
|
||||
height: this.uint32(),
|
||||
width: this.uint32(),
|
||||
depth: this.uint32(),
|
||||
surfaces: this.uint32(),
|
||||
faces: this.uint32(),
|
||||
mipMaps: this.uint32(),
|
||||
metadataSize: this.uint32(),
|
||||
};
|
||||
|
||||
if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch');
|
||||
|
||||
return header;
|
||||
}
|
||||
pixelFormat() {
|
||||
this.seek(4);
|
||||
const usesPresetFormat = this.uint32() === 0;
|
||||
this.seek(-8);
|
||||
|
||||
return usesPresetFormat
|
||||
? (Number(this.uint64()) as PvrPixelFormat)
|
||||
: {
|
||||
order: this.ascii(4),
|
||||
bitRates: times(4, () => this.uint8()),
|
||||
};
|
||||
}
|
||||
resolveBitRatePixelFormat(
|
||||
pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>,
|
||||
) {
|
||||
if (pixelFormat.order === 'rgba') {
|
||||
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
|
||||
return ResolvedBitRatePixelFormat.R4G4B4A4;
|
||||
} else
|
||||
throw new TypeError(
|
||||
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`,
|
||||
);
|
||||
} else if (pixelFormat.order === 'rgb\0') {
|
||||
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
|
||||
return ResolvedBitRatePixelFormat.R5G6B5A0;
|
||||
} else
|
||||
throw new TypeError(
|
||||
`Unhandled rgb bit rate ${pixelFormat.bitRates.join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`);
|
||||
}
|
||||
}
|
||||
metadata() {
|
||||
const fourCC = this.ascii(4);
|
||||
const key = this.uint32();
|
||||
const dataSize = this.uint32();
|
||||
const data = this.consume(dataSize);
|
||||
|
||||
return {
|
||||
fourCC,
|
||||
key,
|
||||
dataSize,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { times } from 'lodash';
|
||||
import { IndividualTankStats } from '../../types/tanksStats';
|
||||
import { BlitzStats } from '../statistics/compositeStats/constants';
|
||||
import { ReadStream, WriteStream } from './buffer';
|
||||
|
||||
export interface BlitzkitTankStats extends BlitzStats {
|
||||
id: number;
|
||||
battle_life_time: number;
|
||||
last_battle_time: number;
|
||||
mark_of_mastery: number;
|
||||
}
|
||||
|
||||
export class RtsmReadStream extends ReadStream {
|
||||
rtsm() {
|
||||
this.magic();
|
||||
this.header();
|
||||
return this.body();
|
||||
}
|
||||
|
||||
magic() {
|
||||
const magic = this.utf8(4);
|
||||
|
||||
if (magic !== 'RTSM') {
|
||||
throw new Error(`Invalid RTSM magic number: "${magic}"`);
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
return {
|
||||
version: this.uint16(),
|
||||
};
|
||||
}
|
||||
|
||||
body() {
|
||||
return {
|
||||
time: this.uint32(),
|
||||
bases: this.uint8(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RtsmWriteStream extends WriteStream {
|
||||
rtsm(bases: number) {
|
||||
this.magic();
|
||||
this.header();
|
||||
this.body(bases);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
magic() {
|
||||
this.utf8('RTSM');
|
||||
}
|
||||
|
||||
header() {
|
||||
this.uint16(1);
|
||||
}
|
||||
|
||||
body(base: number) {
|
||||
this.uint32(Math.round(Date.now() / 1000));
|
||||
this.uint8(base);
|
||||
}
|
||||
}
|
||||
|
||||
export class RtscReadStream extends ReadStream {
|
||||
rtsc() {
|
||||
this.magic();
|
||||
const header = this.header();
|
||||
return this.body(header.playerCount);
|
||||
}
|
||||
|
||||
magic() {
|
||||
const magic = this.utf8(4);
|
||||
|
||||
if (magic !== 'RTSC') {
|
||||
throw new Error(`Invalid RTSC magic number: "${magic}"`);
|
||||
}
|
||||
}
|
||||
|
||||
header() {
|
||||
return {
|
||||
version: this.uint16(),
|
||||
playerCount: this.uint32(),
|
||||
};
|
||||
}
|
||||
|
||||
body(playerCount: number) {
|
||||
return times(playerCount, () => this.player());
|
||||
}
|
||||
|
||||
player() {
|
||||
const id = this.uint32();
|
||||
const tankCount = this.uint16();
|
||||
const tanks = times(tankCount, () => this.tank());
|
||||
|
||||
return {
|
||||
id,
|
||||
tankCount,
|
||||
tanks,
|
||||
};
|
||||
}
|
||||
|
||||
tank() {
|
||||
return {
|
||||
id: this.uint32(),
|
||||
battle_life_time: this.uint32(),
|
||||
last_battle_time: this.uint32(),
|
||||
mark_of_mastery: this.uint8(),
|
||||
battles: this.uint32(),
|
||||
capture_points: this.uint32(),
|
||||
damage_dealt: this.uint32(),
|
||||
damage_received: this.uint32(),
|
||||
dropped_capture_points: this.uint32(),
|
||||
frags: this.uint32(),
|
||||
frags8p: this.uint32(),
|
||||
hits: this.uint32(),
|
||||
losses: this.uint32(),
|
||||
max_frags: this.uint8(),
|
||||
max_xp: this.uint16(),
|
||||
shots: this.uint32(),
|
||||
spotted: this.uint32(),
|
||||
survived_battles: this.uint32(),
|
||||
win_and_survived: this.uint32(),
|
||||
wins: this.uint32(),
|
||||
xp: this.uint32(),
|
||||
} satisfies BlitzkitTankStats;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RtscWritePlayer {
|
||||
id: number;
|
||||
tanks: IndividualTankStats[];
|
||||
}
|
||||
|
||||
export class RtscWriteStream extends WriteStream {
|
||||
rtsc(players: RtscWritePlayer[]) {
|
||||
this.magic();
|
||||
this.header(players.length);
|
||||
this.body(players);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
magic() {
|
||||
this.utf8('RTSC');
|
||||
}
|
||||
|
||||
header(playerCount: number) {
|
||||
this.uint16(1);
|
||||
this.uint32(playerCount);
|
||||
}
|
||||
|
||||
body(players: RtscWritePlayer[]) {
|
||||
players.forEach((player) => {
|
||||
this.uint32(player.id);
|
||||
this.uint16(player.tanks.length);
|
||||
|
||||
player.tanks.forEach((tank) => {
|
||||
this.uint32(tank.tank_id);
|
||||
this.uint32(tank.battle_life_time);
|
||||
this.uint32(tank.last_battle_time);
|
||||
this.uint8(tank.mark_of_mastery);
|
||||
this.uint32(tank.all.battles);
|
||||
this.uint32(tank.all.capture_points);
|
||||
this.uint32(tank.all.damage_dealt);
|
||||
this.uint32(tank.all.damage_received);
|
||||
this.uint32(tank.all.dropped_capture_points);
|
||||
this.uint32(tank.all.frags);
|
||||
this.uint32(tank.all.frags8p);
|
||||
this.uint32(tank.all.hits);
|
||||
this.uint32(tank.all.losses);
|
||||
this.uint8(tank.all.max_frags);
|
||||
this.uint16(tank.all.max_xp);
|
||||
this.uint32(tank.all.shots);
|
||||
this.uint32(tank.all.spotted);
|
||||
this.uint32(tank.all.survived_battles);
|
||||
this.uint32(tank.all.win_and_survived);
|
||||
this.uint32(tank.all.wins);
|
||||
this.uint32(tank.all.xp);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import { Vector3Tuple, Vector4Tuple } from 'three';
|
||||
import { ScpgReadStream } from './scpg';
|
||||
|
||||
export interface SC2 {
|
||||
'#dataNodes': DataNode[];
|
||||
'#hierarchy': Hierarchy[];
|
||||
'#sceneComponents': SceneComponents;
|
||||
}
|
||||
|
||||
type DataNode = {
|
||||
'##name': 'NMaterial';
|
||||
'#id': ArrayBuffer;
|
||||
materialName: string;
|
||||
parentMaterialKey?: bigint;
|
||||
qualityGroup?: string;
|
||||
fxName?: string;
|
||||
|
||||
textures?: Textures;
|
||||
} & (
|
||||
| ({
|
||||
configCount: number;
|
||||
} & Record<`configArchive_${number}`, ConfigArchive>)
|
||||
| {
|
||||
configCount: undefined;
|
||||
}
|
||||
);
|
||||
|
||||
interface ConfigArchive {
|
||||
configName: string;
|
||||
fxName: string;
|
||||
properties: Properties;
|
||||
textureSampleStates: TextureSampleStates;
|
||||
textures: Textures;
|
||||
customCullMode?: number;
|
||||
enabledPresets?: EnabledPresets;
|
||||
flags?: Flags;
|
||||
}
|
||||
|
||||
interface EnabledPresets {
|
||||
AlphaTest: boolean;
|
||||
}
|
||||
|
||||
interface Flags {
|
||||
AMBIENT_ATTENUATION_BOX: number;
|
||||
BLENDING: number;
|
||||
}
|
||||
|
||||
interface Properties {
|
||||
alphatestThreshold: ArrayBuffer;
|
||||
attenuationBoxHalfSize: ArrayBuffer;
|
||||
attenuationBoxSmoothness: ArrayBuffer;
|
||||
baseColorFactor?: ArrayBuffer;
|
||||
decalTileCoordScale?: ArrayBuffer;
|
||||
inGlossiness: ArrayBuffer;
|
||||
inSpecularity: ArrayBuffer;
|
||||
metalFresnelReflectance: ArrayBuffer;
|
||||
metallicRoughnessFactor?: ArrayBuffer;
|
||||
}
|
||||
|
||||
interface TextureSampleStates {
|
||||
albedo: number;
|
||||
baseColorMap: number;
|
||||
baseNormalMap: number;
|
||||
baseRMMap: number;
|
||||
decalmask?: number;
|
||||
maskMap: number;
|
||||
miscMap: number;
|
||||
normalmap: number;
|
||||
}
|
||||
|
||||
export interface Textures {
|
||||
albedo: string;
|
||||
baseColorMap?: string;
|
||||
baseNormalMap?: string;
|
||||
baseRMMap?: string;
|
||||
decalmask?: string;
|
||||
maskMap?: string;
|
||||
miscMap?: string;
|
||||
normalmap?: string;
|
||||
}
|
||||
|
||||
export interface Hierarchy {
|
||||
'##name': 'Entity';
|
||||
'#hierarchy'?: Hierarchy[];
|
||||
components: Components;
|
||||
flags: number;
|
||||
id: bigint;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 0001, 0002, 0003...: Component
|
||||
type Components = {
|
||||
count: number;
|
||||
} & Record<string, Component>;
|
||||
|
||||
// 0001, 0002, 0003...
|
||||
type RoBatches = Record<
|
||||
string,
|
||||
{
|
||||
'##name': 'RenderBatch';
|
||||
'rb.aabbox': RbAabbox;
|
||||
'rb.classname': 'RenderBatch';
|
||||
'rb.datasource': bigint;
|
||||
'rb.nmatname': bigint;
|
||||
'rb.sortingKey': number;
|
||||
}
|
||||
>;
|
||||
|
||||
interface RbAabbox {
|
||||
minimum: number[];
|
||||
maximum: number[];
|
||||
}
|
||||
|
||||
export type Component =
|
||||
| {
|
||||
'comp.typename': 'TransformComponent';
|
||||
'tc.localRotation': Vector4Tuple;
|
||||
'tc.localScale': Vector3Tuple;
|
||||
'tc.localTranslation': Vector3Tuple;
|
||||
'tc.worldRotation': Vector4Tuple;
|
||||
'tc.worldScale': Vector3Tuple;
|
||||
'tc.worldTranslation': Vector3Tuple;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'RenderComponent';
|
||||
'rc.renderObj': {
|
||||
'##name': 'Mesh';
|
||||
'ro.batchCount': number;
|
||||
'ro.batches': RoBatches;
|
||||
'ro.debugflags': number;
|
||||
'ro.flags': number;
|
||||
'ro.notShadowOnly': boolean;
|
||||
'ro.sOclCull': boolean;
|
||||
'ro.sOclIndex': number;
|
||||
} & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'SlotComponent';
|
||||
'sc.attachmentRotation': number[];
|
||||
'sc.attachmentScale': number[];
|
||||
'sc.attachmentTranslation': number[];
|
||||
'sc.configFilePath': string;
|
||||
'sc.slotName': string;
|
||||
'sc.template': string;
|
||||
'sc.typeFiltersCount': number;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'LodComponent';
|
||||
'lc.loddist': Record<`distance${number}`, number>;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'DecorItemComponent';
|
||||
drawOrder: number;
|
||||
shouldApplyCamo: boolean;
|
||||
vanishTargetAlpha: number;
|
||||
vanishWhenArmorShown: boolean;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'NewSlotComponent';
|
||||
slotItemsCount: number;
|
||||
slots: {
|
||||
[key: string]: {
|
||||
configFilePath: string;
|
||||
name: string;
|
||||
transform: {
|
||||
position: Vector3Tuple;
|
||||
scale: Vector3Tuple;
|
||||
rotation: Vector4Tuple;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'ScenarioComponent';
|
||||
'scenario.scriptId': string;
|
||||
}
|
||||
| ({
|
||||
'comp.typename': 'StateSwitcherComponent';
|
||||
'ssc.activeState': number;
|
||||
'ssc.statesCount': number;
|
||||
} & Record<`ssc.state${number}`, string>)
|
||||
| {
|
||||
'comp.typename': 'CustomPropertiesComponent';
|
||||
'cpc.properties.archive': {
|
||||
'editor.donotremove': number;
|
||||
};
|
||||
}
|
||||
| ({
|
||||
'comp.typename': 'ActionComponent';
|
||||
'ac.actionCount': number;
|
||||
} & Record<
|
||||
string, // 0000, 0001, ...
|
||||
{
|
||||
'act.delay': number;
|
||||
'act.delayVariation': number;
|
||||
'act.entityName': string;
|
||||
'act.event': number;
|
||||
'act.eventToTrigger': string;
|
||||
'act.motionFrameTime': number;
|
||||
'act.motionSpeed': number;
|
||||
'act.stateActivated': number;
|
||||
'act.stopAfterNRepeats': number;
|
||||
'act.stopWhenEmpty': boolean;
|
||||
'act.switchIndex': number;
|
||||
'act.type': number;
|
||||
'act.userEventId': string;
|
||||
}
|
||||
>)
|
||||
| {
|
||||
'comp.typename': 'ParticleEffectComponent';
|
||||
'pe.clearOnRestart': boolean;
|
||||
'pe.effectDuration': number;
|
||||
'pe.emitters': {
|
||||
'##name': string;
|
||||
'emitter.data': {
|
||||
'emitter.id': `${number}`;
|
||||
'emitter.quality': string;
|
||||
}[];
|
||||
'emitter.position': Vector3Tuple;
|
||||
'emitter.rotation': Vector4Tuple;
|
||||
'emitter.scale': Vector3Tuple;
|
||||
}[];
|
||||
'pe.inheritScale': boolean;
|
||||
'pe.nestedEmitters': boolean;
|
||||
'pe.repeatsCount': number;
|
||||
'pe.startFromTime': number;
|
||||
'pe.stopWhenEmpty': boolean;
|
||||
'pe.version': number;
|
||||
'ro.flags': number;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'TankElementComponent';
|
||||
isDestructible: boolean;
|
||||
shouldCastShadow: boolean;
|
||||
shouldGenerateSilhouette: boolean;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'MotionComponent';
|
||||
'motion.filepath': string;
|
||||
'motion.playbackRate': number;
|
||||
'simpleMotion.repeatsCount': number;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'SkeletonComponent';
|
||||
joints: Record<
|
||||
string, // 0000, 0001, ...
|
||||
{
|
||||
'joint.bbox.max': Vector3Tuple;
|
||||
'joint.bbox.min': Vector3Tuple;
|
||||
'joint.bindPose': [
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
];
|
||||
'joint.invBindPose': [
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
Vector4Tuple,
|
||||
];
|
||||
'joint.name': string;
|
||||
'joint.parentIndex': number;
|
||||
'joint.uid': string;
|
||||
}
|
||||
>;
|
||||
jointsCount: number;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'AnimationComponent';
|
||||
animation: bigint;
|
||||
animationTimeScale: number;
|
||||
repeatsCount: number;
|
||||
}
|
||||
| {
|
||||
'comp.typename': 'SkeletonJointBindingComponent';
|
||||
count: number;
|
||||
map: Record<
|
||||
string, // 0000, 0001, ...
|
||||
{
|
||||
Uid: string;
|
||||
jointUid: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
interface SceneComponents {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class Sc2ReadStream extends ScpgReadStream {
|
||||
header() {
|
||||
return {
|
||||
name: this.ascii(4),
|
||||
version: this.uint32(),
|
||||
nodeCount: this.uint32(),
|
||||
};
|
||||
}
|
||||
|
||||
versionTags() {
|
||||
return this.ka();
|
||||
}
|
||||
|
||||
descriptor() {
|
||||
const size = this.uint32();
|
||||
const fileType = this.uint32();
|
||||
|
||||
// other felids that we don't have any info about
|
||||
this.seek(size - 4);
|
||||
|
||||
return { size, fileType };
|
||||
}
|
||||
|
||||
sc2() {
|
||||
this.header();
|
||||
this.versionTags();
|
||||
this.descriptor();
|
||||
|
||||
return this.ka() as SC2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { ScpgReadStream, VertexAttribute } from './scpg';
|
||||
|
||||
interface PolygonGroupRaw {
|
||||
'##name': 'PolygonGroup';
|
||||
'#id': ArrayBuffer;
|
||||
cubeTextureCoordCount: number;
|
||||
indexCount: number;
|
||||
indexFormat: 0 | 1;
|
||||
indices: ArrayBuffer;
|
||||
packing: 0 | 1;
|
||||
primitiveCount: number;
|
||||
rhi_primitiveType: 1 | 2 | 10;
|
||||
textureCoordCount: number;
|
||||
vertexCount: number;
|
||||
vertexFormat: number;
|
||||
vertices: ArrayBuffer;
|
||||
}
|
||||
|
||||
type BlitzkitVertex = { attribute: VertexAttribute; value: number[] }[];
|
||||
|
||||
interface BlitzkitPolygonGroup {
|
||||
vertices: BlitzkitVertex[];
|
||||
indices: number[];
|
||||
}
|
||||
|
||||
const vertexAttributesArray = Object.values(VertexAttribute).filter(
|
||||
(value) => typeof value === 'number',
|
||||
);
|
||||
|
||||
export const vertexAttributeVectorSizes = {
|
||||
[VertexAttribute.VERTEX]: 3,
|
||||
[VertexAttribute.NORMAL]: 3,
|
||||
[VertexAttribute.COLOR]: 1,
|
||||
[VertexAttribute.TEXCOORD0]: 2,
|
||||
[VertexAttribute.TEXCOORD1]: 2,
|
||||
[VertexAttribute.TEXCOORD2]: 2,
|
||||
[VertexAttribute.TEXCOORD3]: 2,
|
||||
[VertexAttribute.TANGENT]: 3,
|
||||
[VertexAttribute.BINORMAL]: 3,
|
||||
[VertexAttribute.HARD_JOINTINDEX]: 1,
|
||||
[VertexAttribute.CUBETEXCOORD0]: 3,
|
||||
[VertexAttribute.CUBETEXCOORD1]: 3,
|
||||
[VertexAttribute.CUBETEXCOORD2]: 3,
|
||||
[VertexAttribute.CUBETEXCOORD3]: 3,
|
||||
[VertexAttribute.PIVOT4]: 4,
|
||||
[VertexAttribute.PIVOT_DEPRECATED]: 3,
|
||||
[VertexAttribute.FLEXIBILITY]: 1,
|
||||
[VertexAttribute.ANGLE_SIN_COS]: 2,
|
||||
[VertexAttribute.JOINTINDEX]: 4,
|
||||
[VertexAttribute.JOINTWEIGHT]: 4,
|
||||
} as const;
|
||||
|
||||
export class ScgReadStream extends ScpgReadStream {
|
||||
header() {
|
||||
return {
|
||||
name: this.ascii(4),
|
||||
version: this.uint32(),
|
||||
nodeCount: this.uint32(),
|
||||
nodeCount2: this.uint32(),
|
||||
};
|
||||
}
|
||||
|
||||
scg() {
|
||||
const header = this.header();
|
||||
const polygonGroupsRaw: PolygonGroupRaw[] = [];
|
||||
const polygonGroups = new Map<bigint, BlitzkitPolygonGroup>();
|
||||
|
||||
for (let index = 0; index < header.nodeCount; index++) {
|
||||
const polygonGroupRaw = this.ka() as PolygonGroupRaw;
|
||||
polygonGroupsRaw.push(polygonGroupRaw);
|
||||
}
|
||||
|
||||
polygonGroupsRaw.forEach((polygonGroupRaw) => {
|
||||
const indicesStream = new ScpgReadStream(polygonGroupRaw.indices);
|
||||
const indices: number[] = [];
|
||||
|
||||
for (let index = 0; index < polygonGroupRaw.indexCount; index++) {
|
||||
indices.push(
|
||||
polygonGroupRaw.indexFormat === 0
|
||||
? indicesStream.uint16()
|
||||
: indicesStream.uint32(),
|
||||
);
|
||||
}
|
||||
|
||||
const verticesStream = new ScpgReadStream(polygonGroupRaw.vertices);
|
||||
const vertices: BlitzkitVertex[] = [];
|
||||
const { format, stride } = ScgReadStream.parseVertexFormat(
|
||||
polygonGroupRaw.vertexFormat,
|
||||
);
|
||||
const strideBasedSize = stride * polygonGroupRaw.vertexCount;
|
||||
|
||||
if (verticesStream.buffer.byteLength !== strideBasedSize) {
|
||||
console.warn(
|
||||
`Vertex stride mismatch; expected ${strideBasedSize}, got ${verticesStream.buffer.byteLength}; skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = 0; index < polygonGroupRaw.vertexCount; index++) {
|
||||
vertices[index] = format.map((attribute) => ({
|
||||
attribute,
|
||||
value: verticesStream.vectorN(vertexAttributeVectorSizes[attribute]),
|
||||
}));
|
||||
}
|
||||
|
||||
polygonGroups.set(
|
||||
new DataView(polygonGroupRaw['#id']).getBigUint64(0, true),
|
||||
{
|
||||
vertices,
|
||||
indices,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return polygonGroups;
|
||||
}
|
||||
|
||||
static parseVertexFormat(formatInt: number) {
|
||||
const format: VertexAttribute[] = [];
|
||||
let stride = 0;
|
||||
|
||||
vertexAttributesArray.forEach((attributeUntyped) => {
|
||||
const attribute = attributeUntyped as VertexAttribute;
|
||||
const mask = 1 << attribute;
|
||||
|
||||
if (formatInt & mask) {
|
||||
format.push(attribute);
|
||||
stride += vertexAttributeVectorSizes[attribute] * 4;
|
||||
}
|
||||
});
|
||||
|
||||
return { format, stride };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { times } from 'lodash';
|
||||
import { ReadStream } from './buffer';
|
||||
|
||||
enum KAType {
|
||||
NONE = 0,
|
||||
BOOLEAN = 1,
|
||||
INT32 = 2,
|
||||
FLOAT = 3,
|
||||
STRING = 4,
|
||||
WIDE_STRING = 5,
|
||||
BYTE_ARRAY = 6,
|
||||
UINT32 = 7,
|
||||
KEYED_ARCHIVE = 8,
|
||||
INT64 = 9,
|
||||
UINT64 = 10,
|
||||
VECTOR2 = 11,
|
||||
VECTOR3 = 12,
|
||||
VECTOR4 = 13,
|
||||
MATRIX2 = 14,
|
||||
MATRIX3 = 15,
|
||||
MATRIX4 = 16,
|
||||
COLOR = 17,
|
||||
FASTNAME = 18,
|
||||
AABBOX3 = 19,
|
||||
FILEPATH = 20,
|
||||
FLOAT64 = 21,
|
||||
INT8 = 22,
|
||||
UINT8 = 23,
|
||||
INT16 = 24,
|
||||
UINT16 = 25,
|
||||
ARRAY = 27,
|
||||
TRANSFORM = 29,
|
||||
}
|
||||
|
||||
export enum VertexAttribute {
|
||||
VERTEX = 0,
|
||||
NORMAL = 1,
|
||||
COLOR = 2,
|
||||
TEXCOORD0 = 3,
|
||||
TEXCOORD1 = 4,
|
||||
TEXCOORD2 = 5,
|
||||
TEXCOORD3 = 6,
|
||||
TANGENT = 7,
|
||||
BINORMAL = 8,
|
||||
HARD_JOINTINDEX = 9,
|
||||
PIVOT4 = 10,
|
||||
PIVOT_DEPRECATED = 11,
|
||||
FLEXIBILITY = 12,
|
||||
ANGLE_SIN_COS = 13,
|
||||
JOINTINDEX = 14,
|
||||
JOINTWEIGHT = 15,
|
||||
CUBETEXCOORD0 = 16,
|
||||
CUBETEXCOORD1 = 17,
|
||||
CUBETEXCOORD2 = 18,
|
||||
CUBETEXCOORD3 = 19,
|
||||
}
|
||||
|
||||
export class ScpgReadStream extends ReadStream {
|
||||
vectorN(size: number) {
|
||||
return times(size, () => this.float32());
|
||||
}
|
||||
vector2() {
|
||||
return this.vectorN(2);
|
||||
}
|
||||
vector3() {
|
||||
return this.vectorN(3);
|
||||
}
|
||||
vector4() {
|
||||
return this.vectorN(4);
|
||||
}
|
||||
|
||||
matrixN(size: number) {
|
||||
return times(size, () => this.vectorN(size));
|
||||
}
|
||||
matrix2() {
|
||||
return this.matrixN(2);
|
||||
}
|
||||
matrix3() {
|
||||
return this.matrixN(3);
|
||||
}
|
||||
matrix4() {
|
||||
return this.matrixN(4);
|
||||
}
|
||||
|
||||
kaValue(
|
||||
type: KAType = this.uint8(),
|
||||
stringTable?: Record<number, string>,
|
||||
): unknown {
|
||||
switch (type) {
|
||||
case KAType.NONE:
|
||||
return undefined;
|
||||
|
||||
case KAType.FILEPATH:
|
||||
case KAType.STRING: {
|
||||
if (stringTable) {
|
||||
const id = this.uint32();
|
||||
return stringTable[id];
|
||||
} else {
|
||||
const length = this.uint32();
|
||||
return this.ascii(length);
|
||||
}
|
||||
}
|
||||
|
||||
case KAType.BYTE_ARRAY: {
|
||||
const length = this.uint32();
|
||||
return this.byte(length);
|
||||
}
|
||||
|
||||
case KAType.FLOAT:
|
||||
return this.float32();
|
||||
|
||||
case KAType.ARRAY: {
|
||||
const length = this.uint32();
|
||||
const value = [];
|
||||
|
||||
for (let index = 0; index < length; index++) {
|
||||
value.push(this.kaValue(undefined, stringTable));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
case KAType.KEYED_ARCHIVE: {
|
||||
this.seek(4); // UInt32 length of the nested archive in bytes
|
||||
|
||||
return this.ka(stringTable);
|
||||
}
|
||||
|
||||
case KAType.INT8:
|
||||
return this.int8();
|
||||
case KAType.INT16:
|
||||
return this.int16();
|
||||
case KAType.INT32:
|
||||
return this.int32();
|
||||
case KAType.INT64:
|
||||
return this.int64();
|
||||
case KAType.UINT8:
|
||||
return this.uint8();
|
||||
case KAType.UINT16:
|
||||
return this.uint16();
|
||||
case KAType.UINT32:
|
||||
return this.uint32();
|
||||
case KAType.UINT64:
|
||||
return this.uint64();
|
||||
|
||||
case KAType.BOOLEAN:
|
||||
return this.boolean();
|
||||
|
||||
case KAType.VECTOR2:
|
||||
return this.vector2();
|
||||
case KAType.VECTOR3:
|
||||
return this.vector3();
|
||||
case KAType.VECTOR4:
|
||||
return this.vector4();
|
||||
|
||||
case KAType.COLOR:
|
||||
return this.vector4(); // as RGBA
|
||||
|
||||
case KAType.MATRIX2:
|
||||
return this.matrix2();
|
||||
case KAType.MATRIX3:
|
||||
return this.matrix3();
|
||||
case KAType.MATRIX4:
|
||||
return this.matrix4();
|
||||
|
||||
case KAType.AABBOX3: {
|
||||
const minimum = this.vector3();
|
||||
const maximum = this.vector3();
|
||||
|
||||
return { minimum, maximum };
|
||||
}
|
||||
|
||||
case KAType.FASTNAME: {
|
||||
if (!stringTable) throw new Error('No string table provided');
|
||||
const index = this.uint32();
|
||||
return stringTable[index];
|
||||
}
|
||||
|
||||
case KAType.TRANSFORM: {
|
||||
const position = this.vector3();
|
||||
const scale = this.vector3();
|
||||
const rotation = this.vector4();
|
||||
|
||||
return {
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Unhandled KA type: ${type} (${KAType[type] ?? 'unknown type'})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
ka(stringTable?: Record<number, string>) {
|
||||
this.seek(2); // "KA"
|
||||
const version = this.uint16();
|
||||
const pairs: Record<string, any> = {};
|
||||
|
||||
if (version === 0x0001) {
|
||||
const count = this.uint32();
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
const name = this.kaValue(undefined, stringTable) as string;
|
||||
const value = this.kaValue(undefined, stringTable);
|
||||
|
||||
pairs[name] = value;
|
||||
}
|
||||
} else if (version === 0x0002) {
|
||||
const count = this.uint32();
|
||||
const strings: string[] = [];
|
||||
const stringTable: Record<number, string> = {};
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
const length = this.uint16();
|
||||
const string = this.ascii(length);
|
||||
|
||||
strings.push(string);
|
||||
}
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
const id = this.uint32();
|
||||
stringTable[id] = strings[index];
|
||||
}
|
||||
|
||||
const nodeCount = this.uint32();
|
||||
|
||||
for (let index = 0; index < nodeCount; index++) {
|
||||
const keyId = this.uint32();
|
||||
const value = this.kaValue(undefined, stringTable);
|
||||
const key = stringTable[keyId];
|
||||
|
||||
pairs[key] = value;
|
||||
}
|
||||
} else if (version === 0x0102) {
|
||||
if (!stringTable) throw new Error('No string table provided');
|
||||
|
||||
const count = this.uint32();
|
||||
|
||||
for (let index = 0; index < count; index++) {
|
||||
const keyIndex = this.uint32();
|
||||
const key = stringTable[keyIndex];
|
||||
const valueType = this.uint8();
|
||||
|
||||
if (valueType === KAType.STRING) {
|
||||
const valueIndex = this.uint32();
|
||||
const value = stringTable[valueIndex];
|
||||
pairs[key] = value;
|
||||
} else {
|
||||
const value = this.kaValue(valueType, stringTable);
|
||||
pairs[key] = value;
|
||||
}
|
||||
}
|
||||
} else if (version === 0xff02) {
|
||||
// same as 0x0102 but empty and with no count
|
||||
return {};
|
||||
} else throw new RangeError(`Unhandled KA version: ${version}`);
|
||||
|
||||
return pairs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ReadStream } from './buffer';
|
||||
|
||||
export class WindowsReadStream extends ReadStream {
|
||||
dword() {
|
||||
return this.uint32();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// MIGRATION TODO: i added this use client, it makes stuff work again?
|
||||
'use client';
|
||||
|
||||
import { Draft, produce } from 'immer';
|
||||
import {
|
||||
createContext,
|
||||
|
||||
Reference in new issue
Block a user