patchwork-mapconverter
Version:
Executable wrapper for https://github.com/ChiefOfGxBxL/WC3MapTranslator
290 lines (251 loc) • 11.5 kB
text/typescript
import { HexBuffer } from '../HexBuffer'
import { W3Buffer } from '../W3Buffer'
import { type WarResult, type JsonResult } from '../CommonInterfaces'
import { RandomSpawn, type Unit } from '../data/Unit'
import { type Translator } from './Translator'
import { type UnitSet } from '../data/UnitSet'
export class UnitsTranslator implements Translator<Unit[]> {
private static instance: UnitsTranslator
private constructor() { }
public static getInstance(): UnitsTranslator {
if (this.instance == null) {
this.instance = new this()
}
return this.instance
}
public static jsonToWar(units: Unit[]): WarResult {
return this.getInstance().jsonToWar(units)
}
public static warToJson(buffer: Buffer): JsonResult<Unit[]> {
return this.getInstance().warToJson(buffer)
}
public jsonToWar(unitsJson: Unit[]): WarResult {
const outBufferToWar = new HexBuffer()
/*
* Header
*/
outBufferToWar.addChars('W3do')
outBufferToWar.addInt(9)
outBufferToWar.addInt(11)
outBufferToWar.addInt(unitsJson?.length || 0) // number of units
/*
* Body
*/
unitsJson?.forEach((unit) => {
outBufferToWar.addChars(unit.type) // type
outBufferToWar.addInt(unit.variation != null ? unit.variation : 0) // variation
outBufferToWar.addFloat(unit.position[0]) // position x
outBufferToWar.addFloat(unit.position[1]) // position y
outBufferToWar.addFloat(unit.position[2]) // position z
outBufferToWar.addFloat(unit.rotation != null ? unit.rotation : 0) // rotation angle
if (unit.scale == null) unit.scale = [1, 1, 1]
outBufferToWar.addFloat(unit.scale[0] != null ? unit.scale[0] : 1) // scale x
outBufferToWar.addFloat(unit.scale[1] != null ? unit.scale[1] : 1) // scale y
outBufferToWar.addFloat(unit.scale[2] != null ? unit.scale[2] : 1) // scale z
outBufferToWar.addChars(unit.skin)
// Unit flags
outBufferToWar.addByte(0) // UNSUPPORTED: flags
outBufferToWar.addInt(unit.player) // player #
outBufferToWar.addByte(0) // (byte unknown - 0)
outBufferToWar.addByte(0) // (byte unknown - 0)
outBufferToWar.addInt(unit.hitpoints) // hitpoints
outBufferToWar.addInt(unit.mana != null ? unit.mana : 0) // mana
outBufferToWar.addInt(unit.randomItemSetPtr)
outBufferToWar.addInt(unit.droppedItemSets?.length || 0)
unit.droppedItemSets?.forEach(itemSet => {
outBufferToWar.addInt(itemSet.items?.length || 0)
itemSet.items?.forEach(item => {
outBufferToWar.addChars(item.itemId)
outBufferToWar.addInt(item.chance)
})
})
// Gold amount
// Required if unit is a gold mine
// Optional (set to zero) if unit is not a gold mine
outBufferToWar.addInt(unit.gold)
// outBufferToWar.addInt(unit.type === 'ngol' ? unit.gold : 0);
outBufferToWar.addFloat(unit.targetAcquisition != null ? unit.targetAcquisition : 0) // target acquisition
// Unit hero attributes
// Can be left unspecified, but values can never be below 1
if (unit.hero == null) unit.hero = { level: 1, str: 1, agi: 1, int: 1 }
outBufferToWar.addInt(unit.hero.level)
outBufferToWar.addInt(unit.hero.str)
outBufferToWar.addInt(unit.hero.agi)
outBufferToWar.addInt(unit.hero.int)
// Inventory - - -
if (unit.inventory == null) unit.inventory = []
outBufferToWar.addInt(unit.inventory?.length || 0) // # items in inventory
unit.inventory?.forEach(item => {
outBufferToWar.addInt(item.slot - 1) // zero-index item slot
outBufferToWar.addChars(item.type)
})
// Modified abilities - - -
if (unit.abilities == null) unit.abilities = []
outBufferToWar.addInt(unit.abilities?.length || 0) // # modified abilities
unit.abilities?.forEach((ability) => {
outBufferToWar.addChars(ability.ability) // ability string
outBufferToWar.addInt(+ability.active) // 0 = not active, 1 = active
outBufferToWar.addInt(ability.level)
})
// Random
outBufferToWar.addInt(unit.random.type)
switch (unit.random.type) {
case 0:
outBufferToWar.addByte(unit.random.level as number)
outBufferToWar.addByte(0) // Unknown - apparently it's part of level ^
outBufferToWar.addByte(0) // Unknown - apparently it's part of level ^
outBufferToWar.addByte(unit.random.itemClass as number)
break
case 1:
outBufferToWar.addInt(unit.random.groupIndex as number)
outBufferToWar.addInt(unit.random.columnIndex as number)
break
case 2:
outBufferToWar.addInt((unit.random.unitSet as UnitSet)?.length || 0)
unit.random.unitSet?.forEach(spawnableUnit => {
outBufferToWar.addChars(spawnableUnit.unitId)
outBufferToWar.addInt(spawnableUnit.chance)
})
break
}
outBufferToWar.addInt(unit.color != null ? unit.color : unit.player) // custom color, defaults to owning player
outBufferToWar.addInt(unit.waygate) // waygate
outBufferToWar.addInt(unit.id) // id
})
return {
errors: [],
buffer: outBufferToWar.getBuffer()
}
}
public warToJson(buffer: Buffer): JsonResult<Unit[]> {
const result: Unit[] = []
const outBufferToJSON = new W3Buffer(buffer)
const fileId = outBufferToJSON.readChars(4) // W3do for doodad file
const fileVersion = outBufferToJSON.readInt() // File version = 7
const subVersion = outBufferToJSON.readInt() // 0B 00 00 00
const numUnits = outBufferToJSON.readInt() // # of units
for (let i = 0; i < numUnits; i++) {
const unit: Unit = {
type: '',
variation: -1,
position: [0, 0, 0],
rotation: 0,
scale: [0, 0, 0],
hero: { level: 1, str: 1, agi: 1, int: 1 },
skin: '',
inventory: [],
abilities: [],
player: 0,
hitpoints: -1,
mana: -1,
randomItemSetPtr: -1,
droppedItemSets: [],
gold: 0,
targetAcquisition: -1,
random: {
type: -1,
} as RandomSpawn,
color: -1,
waygate: -1,
id: -1
}
unit.type = outBufferToJSON.readChars(4) // (iDNR = random item, uDNR = random unit)
unit.variation = outBufferToJSON.readInt()
unit.position = [outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat()] // X Y Z coords
unit.rotation = outBufferToJSON.readFloat()
unit.scale = [outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat()] // X Y Z scaling
if (fileVersion > 7) {
unit.skin = outBufferToJSON.readChars(4)
} else { // default unit's skin - Note: Probably fails for items?
unit.skin = unit.type
}
// UNSUPPORTED: flags
const flags = outBufferToJSON.readByte()
unit.player = outBufferToJSON.readInt() // (player1 = 0, 16=neutral passive); note: wc3 patch now has 24 max players
outBufferToJSON.readByte() // unknown
outBufferToJSON.readByte() // unknown
unit.hitpoints = outBufferToJSON.readInt() // -1 = use default
unit.mana = outBufferToJSON.readInt() // -1 = use default, 0 = unit doesn't have mana
if (subVersion !== 9) { // not RoC
unit.randomItemSetPtr = outBufferToJSON.readInt()
}
const numDroppedItemSets = outBufferToJSON.readInt()
for (let j = 0; j < numDroppedItemSets; j++) {
unit.droppedItemSets.push({ items: [] })
const numDroppableItems = outBufferToJSON.readInt()
for (let k = 0; k < numDroppableItems; k++) {
unit.droppedItemSets[j].items.push({
itemId: outBufferToJSON.readChars(4), // Item ID
chance: outBufferToJSON.readInt() // % chance to drop
})
}
}
unit.gold = outBufferToJSON.readInt()
unit.targetAcquisition = outBufferToJSON.readFloat() // (-1 = normal, -2 = camp)
unit.hero = {
level: outBufferToJSON.readInt(), // non-hero units = 1
str: 0,
agi: 0,
int: 0
}
if (subVersion !== 9) { // not RoC
unit.hero.str = outBufferToJSON.readInt()
unit.hero.agi = outBufferToJSON.readInt()
unit.hero.int = outBufferToJSON.readInt()
}
const numItemsInventory = outBufferToJSON.readInt()
for (let j = 0; j < numItemsInventory; j++) {
unit.inventory.push({
slot: outBufferToJSON.readInt() + 1, // the int is 0-based, but json format wants 1-6
type: outBufferToJSON.readChars(4) // Item ID
})
}
const numModifiedAbil = outBufferToJSON.readInt()
for (let j = 0; j < numModifiedAbil; j++) {
unit.abilities.push({
ability: outBufferToJSON.readChars(4), // Ability ID
active: outBufferToJSON.readInt() === 1, // autocast active? 0=no, 1=active
level: outBufferToJSON.readInt()
})
}
unit.random.type = outBufferToJSON.readInt() // random unit/item flag "r" (for uDNR units and iDNR items)
if (unit.random.type === 0) {
// 0 = Any neutral passive building/item, in this case we have
// byte[3]: level of the random unit/item,-1 = any (this is actually interpreted as a 24-bit number)
// byte: item class of the random item, 0 = any, 1 = permanent ... (this is 0 for units)
// r is also 0 for non random units/items so we have these 4 bytes anyway (even if the id wasnt uDNR or iDNR)
unit.random.level = outBufferToJSON.readByte()
outBufferToJSON.readByte() // unknown
outBufferToJSON.readByte() // unknown
unit.random.itemClass = outBufferToJSON.readByte()
} else if (unit.random.type === 1) {
// 1 = random unit from random group (defined in the w3i), in this case we have
// int: unit group number (which group from the global table)
// int: position number (which column of this group)
// the column should of course have the item flag set (in the w3i) if this is a random item
unit.random.groupIndex = outBufferToJSON.readInt()
unit.random.columnIndex = outBufferToJSON.readInt()
} else if (unit.random.type === 2) {
// 2 = random unit from custom table, in this case we have
// int: number "n" of different available units
// then we have n times a random unit structure
const numDiffAvailUnits = outBufferToJSON.readInt()
unit.random.unitSet = []
for (let k = 0; k < numDiffAvailUnits; k++) {
unit.random.unitSet.push({
unitId: outBufferToJSON.readChars(4), // Unit ID
chance: outBufferToJSON.readInt() // % chance
})
}
}
unit.color = outBufferToJSON.readInt()
unit.waygate = outBufferToJSON.readInt() // waygate (-1 = deactivated, else its the creation number of the target rect as in war3map.w3r)
unit.id = outBufferToJSON.readInt()
result.push(unit)
}
return {
errors: [],
json: result
}
}
}