patchwork-mapconverter
Version:
Executable wrapper for https://github.com/ChiefOfGxBxL/WC3MapTranslator
561 lines (493 loc) • 23.5 kB
text/typescript
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { HexBuffer } from '../HexBuffer'
import { W3Buffer } from '../W3Buffer'
import { type WarResult, type JsonResult } from '../CommonInterfaces'
import { type Translator } from './Translator'
import { FogType, type Force, type Info, type Player, ScriptLanguage, SupportedModes } from '../data/Info'
export class InfoTranslator implements Translator<Info> {
private static instance: InfoTranslator
private constructor() { }
public static getInstance(): InfoTranslator {
if (this.instance == null) {
this.instance = new this()
}
return this.instance
}
public static jsonToWar(info: Info): WarResult {
return this.getInstance().jsonToWar(info)
}
public static warToJson(buffer: Buffer): JsonResult<Info> {
return this.getInstance().warToJson(buffer)
}
public jsonToWar(infoJson: Info): WarResult {
const outBufferToWar = new HexBuffer()
outBufferToWar.addInt(33) // format version
outBufferToWar.addInt(infoJson.saves != null ? infoJson.saves : 0)
outBufferToWar.addInt(infoJson.editorVersion != null ? infoJson.editorVersion : 0)
outBufferToWar.addInt(infoJson.gameVersion.major)
outBufferToWar.addInt(infoJson.gameVersion.minor)
outBufferToWar.addInt(infoJson.gameVersion.patch)
outBufferToWar.addInt(infoJson.gameVersion.build)
// Map information
outBufferToWar.addString(infoJson.map.name)
outBufferToWar.addString(infoJson.map.author)
outBufferToWar.addString(infoJson.map.description)
outBufferToWar.addString(infoJson.map.recommendedPlayers)
// Camera bounds (8 floats total)
for (let cbIndex = 0; cbIndex < 8; cbIndex++) {
outBufferToWar.addFloat(infoJson.camera.bounds[cbIndex])
}
// Camera complements (4 ints total)
for (let ccIndex = 0; ccIndex < 4; ccIndex++) {
outBufferToWar.addInt(infoJson.camera.complements[ccIndex])
}
// Playable area
outBufferToWar.addInt(infoJson.map.playableArea.width)
outBufferToWar.addInt(infoJson.map.playableArea.height)
/*
* Flags
*/
let flags = 0
if (infoJson.map.flags != null) { // can leave out the entire flags object, all flags will default to false
if (infoJson.map.flags.hideMinimapInPreview) flags |= 0x0001 // hide minimap in preview screens
if (infoJson.map.flags.modifyAllyPriorities) flags |= 0x0002 // modify ally priorities
if (infoJson.map.flags.isMeleeMap) flags |= 0x0004 // melee map
if (infoJson.map.flags.nonDefaultTilesetMapSizeLargeNeverBeenReducedToMedium) flags |= 0x0008 // playable map size was large and never reduced to medium (?)
if (infoJson.map.flags.maskedPartiallyVisible) flags |= 0x0010 // masked area are partially visible
if (infoJson.map.flags.fixedPlayerSetting) flags |= 0x0020 // fixed player setting for custom forces
if (infoJson.map.flags.useCustomForces) flags |= 0x0040 // use custom forces
if (infoJson.map.flags.useCustomTechtree) flags |= 0x0080 // use custom techtree
if (infoJson.map.flags.useCustomAbilities) flags |= 0x0100 // use custom abilities
if (infoJson.map.flags.useCustomUpgrades) flags |= 0x0200 // use custom upgrades
if (infoJson.map.flags.mapPropertiesMenuOpenedAtLeastOnce) flags |= 0x0400 // map properties menu opened at least once since map creation (?)
if (infoJson.map.flags.waterWavesOnCliffShores) flags |= 0x0800 // show water waves on cliff shores
if (infoJson.map.flags.waterWavesOnRollingShores) flags |= 0x1000 // show water waves on rolling shores
if (infoJson.map.flags.useTerrainFog) flags |= 0x2000
if (infoJson.map.flags.tftRequired) flags |= 0x4000
if (infoJson.map.flags.useItemClassificationSystem) flags |= 0x8000
if (infoJson.map.flags.enableWaterTinting) flags |= 0x10000
if (infoJson.map.flags.useAccurateProbabilityForCalculations) flags |= 0x20000
if (infoJson.map.flags.useCustomAbilitySkins) flags |= 0x40000
if (infoJson.map.flags.disableDenyIcon) flags |= 0x80000
if (infoJson.map.flags.forceDefaultCameraZoom) flags |= 0x100000
if (infoJson.map.flags.forceMaxCameraZoom) flags |= 0x200000
if (infoJson.map.flags.forceMinCameraZoom) flags |= 0x400000
// 0x800000
// 8 -unknown bits?
}
outBufferToWar.addInt(flags) // Add flags
// Map main ground type
outBufferToWar.addChar(infoJson.map.mainTileType)
// Loading screen
outBufferToWar.addInt(infoJson.loadingScreen.background)
outBufferToWar.addString(infoJson.loadingScreen.path)
outBufferToWar.addString(infoJson.loadingScreen.text)
outBufferToWar.addString(infoJson.loadingScreen.title)
outBufferToWar.addString(infoJson.loadingScreen.subtitle)
// Use game data set
outBufferToWar.addInt(infoJson.gameDataSet)
// Prologue
outBufferToWar.addString(infoJson.prologue.path)
outBufferToWar.addString(infoJson.prologue.text)
outBufferToWar.addString(infoJson.prologue.title)
outBufferToWar.addString(infoJson.prologue.subtitle)
// Fog
outBufferToWar.addInt(infoJson.fog.type)
outBufferToWar.addFloat(infoJson.fog.startHeight)
outBufferToWar.addFloat(infoJson.fog.endHeight)
outBufferToWar.addFloat(infoJson.fog.density)
outBufferToWar.addByte(infoJson.fog.color[0])
outBufferToWar.addByte(infoJson.fog.color[1])
outBufferToWar.addByte(infoJson.fog.color[2])
outBufferToWar.addByte(infoJson.fog.color[3])
// Misc.
// // If globalWeather is not defined or is set to 'none', use 0 sentinel value, else add char[4] -- why this distinct crap? it just breaks the w3i for me.
// if (infoJson.globalWeather == null || infoJson.globalWeather.toLowerCase() === 'none') {
// outBufferToWar.addInt(0)
// } else {
outBufferToWar.addInt(infoJson.globalWeather)
// }
outBufferToWar.addString(infoJson.customSoundEnvironment != null ? infoJson.customSoundEnvironment : '')
outBufferToWar.addByte(infoJson.customLightEnv)
// Custom water tinting
outBufferToWar.addByte(infoJson.water[0])
outBufferToWar.addByte(infoJson.water[1])
outBufferToWar.addByte(infoJson.water[2])
outBufferToWar.addByte(infoJson.water[3])
outBufferToWar.addInt(infoJson.scriptLanguage)
outBufferToWar.addInt(infoJson.supportedModes)
outBufferToWar.addInt(infoJson.gameDataVersion)
outBufferToWar.addInt(infoJson.forcedDefaultCamDistance)
outBufferToWar.addInt(infoJson.forcedMaxCamDistance)
outBufferToWar.addInt(infoJson.forcedMinCamDistance)
// Players
outBufferToWar.addInt(infoJson.players?.length || 0)
infoJson.players?.forEach((player) => {
outBufferToWar.addInt(player.playerNum)
outBufferToWar.addInt(player.type)
outBufferToWar.addInt(player.race)
outBufferToWar.addInt(player.startingPos.fixed ? 1 : 0)
outBufferToWar.addString(player.name)
outBufferToWar.addFloat(player.startingPos.x)
outBufferToWar.addFloat(player.startingPos.y)
outBufferToWar.addInt(player.allyLowPriorities) // ally low prio flags
outBufferToWar.addInt(player.allyHighPriorities) // ally high prio flags
outBufferToWar.addInt(player.enemyLowPriorities) // enemy low prio flags
outBufferToWar.addInt(player.enermyHighPriorities) // enemy high prio flags
})
// Forces
outBufferToWar.addInt(infoJson.forces?.length || 0)
infoJson.forces?.forEach((force) => {
// Calculate flags
let forceFlags = 0
if (force.flags.allied) forceFlags |= 0x0001
if (force.flags.alliedVictory) forceFlags |= 0x0002
// Skip 0x0004
if (force.flags.shareVision) forceFlags |= 0x0008
if (force.flags.shareUnitControl) forceFlags |= 0x0010
if (force.flags.shareAdvUnitControl) forceFlags |= 0x0020
outBufferToWar.addInt(forceFlags)
outBufferToWar.addInt(force.players)
outBufferToWar.addString(force.name)
})
// Struct: upgrade avail.
outBufferToWar.addInt(infoJson.upgrades?.length || 0)
infoJson.upgrades?.forEach(upgrade => {
outBufferToWar.addInt(upgrade.playerFlags)
outBufferToWar.addChars(upgrade.upgradeId)
outBufferToWar.addInt(upgrade.level)
outBufferToWar.addInt(upgrade.availability)
})
// Struct: tech avail.
outBufferToWar.addInt(infoJson.techBlacklist?.length || 0)
infoJson.techBlacklist?.forEach(tech => {
outBufferToWar.addInt(tech.playerFlags)
outBufferToWar.addChars(tech.techId)
})
// Struct: random unit table
outBufferToWar.addInt(infoJson.randomUnitTables?.length || 0)
infoJson.randomUnitTables?.forEach(randomUnitTable => {
outBufferToWar.addInt(randomUnitTable.id)
outBufferToWar.addString(randomUnitTable.name)
outBufferToWar.addInt(randomUnitTable.positions?.length || 0)
randomUnitTable.positions?.forEach(position => outBufferToWar.addInt(position))
outBufferToWar.addInt(randomUnitTable.chances?.length || 0)
randomUnitTable.chances?.forEach(chance => {
outBufferToWar.addInt(chance.chance)
chance.unitIds.forEach(unitId => outBufferToWar.addChars(unitId)) //Amount of units must match amount of positions
})
})
// Struct: random item table
outBufferToWar.addInt(infoJson.randomItemTables?.length || 0)
infoJson.randomItemTables?.forEach(randomItemTable => {
outBufferToWar.addInt(randomItemTable.id)
outBufferToWar.addString(randomItemTable.name)
outBufferToWar.addInt(randomItemTable.rows?.length || 0)
randomItemTable.rows?.forEach(randomItemPool => {
outBufferToWar.addInt(randomItemPool.objects?.length || 0)
randomItemPool.objects?.forEach(randomItem => {
outBufferToWar.addInt(randomItem.chance)
outBufferToWar.addChars(randomItem.objectId)
})
})
})
return {
errors: [],
buffer: outBufferToWar.getBuffer()
}
}
public warToJson(buffer: Buffer): JsonResult<Info> {
const result: Info = {
map: {
name: '',
author: '',
description: '',
recommendedPlayers: '',
playableArea: {
width: 64,
height: 64
},
mainTileType: '',
flags: {
hideMinimapInPreview: false, // 0x0001: 1=hide minimap in preview screens
modifyAllyPriorities: true, // 0x0002: 1=modify ally priorities
isMeleeMap: false, // 0x0004: 1=melee map
nonDefaultTilesetMapSizeLargeNeverBeenReducedToMedium: false, // 0x0008: 1=playable map size was large and has never been reduced to medium (?)
maskedPartiallyVisible: false, // 0x0010: 1=masked area are partially visible
fixedPlayerSetting: false, // 0x0020: 1=fixed player setting for custom forces
useCustomForces: false, // 0x0040: 1=use custom forces
useCustomTechtree: false, // 0x0080: 1=use custom techtree
useCustomAbilities: false, // 0x0100: 1=use custom abilities
useCustomUpgrades: false, // 0x0200: 1=use custom upgrades
mapPropertiesMenuOpenedAtLeastOnce: false, // 0x0400: 1=map properties menu opened at least once since map creation (?)
waterWavesOnCliffShores: false, // 0x0800: 1=show water waves on cliff shores
waterWavesOnRollingShores: false, // 0x1000: 1=show water waves on rolling shores
useTerrainFog: false, // 0x2000
tftRequired: false, // 0x4000
useItemClassificationSystem: false, // 0x8000: 1=use item classification system
enableWaterTinting: false, // 0x10000
useAccurateProbabilityForCalculations: false, // 0x20000
useCustomAbilitySkins: false, // 0x40000
disableDenyIcon: false, //0x80000
forceDefaultCameraZoom: false, // 0x100000
forceMaxCameraZoom: false, // 0x200000
forceMinCameraZoom: false // 0x400000
}
},
loadingScreen: {
background: 0,
path: '',
text: '',
title: '',
subtitle: ''
},
prologue: {
path: '',
text: '',
title: '',
subtitle: ''
},
fog: {
type: FogType.Linear,
startHeight: 0,
endHeight: 0,
density: 0,
color: [0, 0, 0, 1]
},
camera: {
bounds: [],
complements: []
},
players: [],
forces: [],
saves: 0,
editorVersion: 0,
scriptLanguage: ScriptLanguage.JASS,
supportedModes: SupportedModes.Both,
forcedDefaultCamDistance: 1250,
forcedMaxCamDistance: 1250,
forcedMinCamDistance: 1250,
gameVersion: {
major: 0,
minor: 0,
patch: 0,
build: 0
},
globalWeather: 0,
customSoundEnvironment: '',
customLightEnv: 0,
water: [],
gameDataVersion: 0,
gameDataSet: 0,
upgrades: [],
techBlacklist: [],
randomUnitTables: [],
randomItemTables: []
}
const outBufferToJSON = new W3Buffer(buffer)
const fileVersion = outBufferToJSON.readInt()
result.saves = outBufferToJSON.readInt()
result.editorVersion = outBufferToJSON.readInt()
result.gameVersion = {
major: outBufferToJSON.readInt(),
minor: outBufferToJSON.readInt(),
patch: outBufferToJSON.readInt(),
build: outBufferToJSON.readInt()
}
result.map.name = outBufferToJSON.readString()
result.map.author = outBufferToJSON.readString()
result.map.description = outBufferToJSON.readString()
result.map.recommendedPlayers = outBufferToJSON.readString()
result.camera.bounds = [
outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat(),
outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat(), outBufferToJSON.readFloat()
]
result.camera.complements = [
outBufferToJSON.readInt(), outBufferToJSON.readInt(), outBufferToJSON.readInt(), outBufferToJSON.readInt()
]
result.map.playableArea = {
width: outBufferToJSON.readInt(),
height: outBufferToJSON.readInt()
}
const flags = outBufferToJSON.readInt()
result.map.flags = {
hideMinimapInPreview: !!(flags & 0x0001),
modifyAllyPriorities: !!(flags & 0x0002),
isMeleeMap: !!(flags & 0x0004),
nonDefaultTilesetMapSizeLargeNeverBeenReducedToMedium: !!(flags & 0x0008),
maskedPartiallyVisible: !!(flags & 0x0010),
fixedPlayerSetting: !!(flags & 0x0020),
useCustomForces: !!(flags & 0x0040),
useCustomTechtree: !!(flags & 0x0080),
useCustomAbilities: !!(flags & 0x0100),
useCustomUpgrades: !!(flags & 0x0200),
mapPropertiesMenuOpenedAtLeastOnce: !!(flags & 0x0400),
waterWavesOnCliffShores: !!(flags & 0x0800),
waterWavesOnRollingShores: !!(flags & 0x1000),
useTerrainFog: !!(flags & 0x2000),
tftRequired: !!(flags & 0x4000),
useItemClassificationSystem: !!(flags & 0x8000),
enableWaterTinting: !!(flags & 0x10000),
useAccurateProbabilityForCalculations: !!(flags & 0x20000),
useCustomAbilitySkins: !!(flags & 0x40000),
disableDenyIcon: !!(flags & 0x80000),
forceDefaultCameraZoom: !!(flags & 0x100000),
forceMaxCameraZoom: !!(flags & 0x200000),
forceMinCameraZoom: !!(flags & 0x400000)
}
result.map.mainTileType = outBufferToJSON.readChars()
result.loadingScreen.background = outBufferToJSON.readInt()
result.loadingScreen.path = outBufferToJSON.readString()
result.loadingScreen.text = outBufferToJSON.readString()
result.loadingScreen.title = outBufferToJSON.readString()
result.loadingScreen.subtitle = outBufferToJSON.readString()
result.gameDataSet = outBufferToJSON.readInt() // 0 = standard
result.prologue = {
path: outBufferToJSON.readString(),
text: outBufferToJSON.readString(),
title: outBufferToJSON.readString(),
subtitle: outBufferToJSON.readString()
}
result.fog = {
type: outBufferToJSON.readInt(),
startHeight: outBufferToJSON.readFloat(),
endHeight: outBufferToJSON.readFloat(),
density: outBufferToJSON.readFloat(),
color: [outBufferToJSON.readByte(), outBufferToJSON.readByte(), outBufferToJSON.readByte(), outBufferToJSON.readByte()] // R G B A
}
result.globalWeather = outBufferToJSON.readInt()
result.customSoundEnvironment = outBufferToJSON.readString()
result.customLightEnv = outBufferToJSON.readByte()
result.water = [outBufferToJSON.readByte(), outBufferToJSON.readByte(), outBufferToJSON.readByte(), outBufferToJSON.readByte()] // R G B A
result.scriptLanguage = outBufferToJSON.readInt()
result.supportedModes = outBufferToJSON.readInt()
result.gameDataVersion = outBufferToJSON.readInt()
if (fileVersion >= 32){
result.forcedDefaultCamDistance = outBufferToJSON.readInt()
result.forcedMaxCamDistance = outBufferToJSON.readInt()
}
if (fileVersion >= 33){
result.forcedMinCamDistance = outBufferToJSON.readInt()
}
// Struct: players
const numPlayers = outBufferToJSON.readInt()
for (let i = 0; i < numPlayers; i++) {
const player: Player = {
name: '',
startingPos: { x: 0, y: 0, fixed: false },
playerNum: 0,
type: 0,
race: 0,
allyLowPriorities: 0,
allyHighPriorities: 0,
enemyLowPriorities: 0,
enermyHighPriorities: 0
}
player.playerNum = outBufferToJSON.readInt()
player.type = outBufferToJSON.readInt() // 1=Human, 2=Computer, 3=Neutral, 4=Rescuable
player.race = outBufferToJSON.readInt() // 1=Human, 2=Orc, 3=Undead, 4=Night Elf
const isPlayerStartPositionFixed: boolean = outBufferToJSON.readInt() === 1 // 00000001 = fixed start position
player.name = outBufferToJSON.readString()
player.startingPos = {
x: outBufferToJSON.readFloat(),
y: outBufferToJSON.readFloat(),
fixed: isPlayerStartPositionFixed
}
player.allyLowPriorities = outBufferToJSON.readInt() // ally low priorities flags (bit "x"=1 --> set for player "x")
player.allyHighPriorities = outBufferToJSON.readInt() // ally high priorities flags (bit "x"=1 --> set for player "x")
player.enemyLowPriorities = outBufferToJSON.readInt() // enemy low priorities flags
player.enermyHighPriorities = outBufferToJSON.readInt() // enemy high priorities flags
result.players.push(player)
}
// Struct: forces
const numForces = outBufferToJSON.readInt()
for (let i = 0; i < numForces; i++) {
const force: Force = {
flags: { allied: false, alliedVictory: true, shareVision: true, shareUnitControl: false, shareAdvUnitControl: false },
players: 0,
name: ''
}
const forceFlag = outBufferToJSON.readInt()
force.flags = {
allied: !!(forceFlag & 0b1), // 0x00000001: allied (force 1)
alliedVictory: !!(forceFlag & 0b10), // 0x00000002: allied victory
// 0x00000004: share vision (the documentation has this incorrect)
shareVision: !!(forceFlag & 0b1000), // 0x00000008: share vision
shareUnitControl: !!(forceFlag & 0b10000), // 0x00000010: share unit control
shareAdvUnitControl: !!(forceFlag & 0b100000) // 0x00000020: share advanced unit control
}
force.players = outBufferToJSON.readInt() // UNSUPPORTED: (bit "x"=1 --> player "x" is in this force; but carried over for accurate translation
force.name = outBufferToJSON.readString()
result.forces.push(force)
}
// Struct: upgrade avail.
const numUpgrades = outBufferToJSON.readInt()
for (let i = 0; i < numUpgrades; i++) {
result.upgrades.push({
playerFlags: outBufferToJSON.readInt(), // Player Flags (bit "x"=1 if this change applies for player "x")
upgradeId: outBufferToJSON.readChars(4), // upgrade id (as in UpgradeData.slk)
level: outBufferToJSON.readInt(), // Level of the upgrade for which the availability is changed (this is actually the level - 1, so 1 => 0)
availability: outBufferToJSON.readInt() // Availability (0 = unavailable, 1 = available, 2 = researched)
})
}
// Struct: tech avail.
const numTech = outBufferToJSON.readInt()
for (let i = 0; i < numTech; i++) {
result.techBlacklist.push({
playerFlags: outBufferToJSON.readInt(), // Player Flags (bit "x"=1 if this change applies for player "x")
techId: outBufferToJSON.readChars(4) // tech id (this can be an item, unit or ability)
})
}
// Struct: random unit table
const numUnitTable = outBufferToJSON.readInt()
for (let i = 0; i < numUnitTable; i++) {
result.randomUnitTables.push({
id: outBufferToJSON.readInt(), // Group number
name: outBufferToJSON.readString(), // Group name
positions: [],
chances: []
})
const numPositions = outBufferToJSON.readInt() // Number "m" of positions
for (let j = 0; j < numPositions; j++) {
result.randomUnitTables[i].positions.push(outBufferToJSON.readInt()) // Apparently, the following is false: unit table (=0), a building table (=1) or an item table (=2)
}
const numChances = outBufferToJSON.readInt()
for (let j = 0; j < numChances; j++) {
result.randomUnitTables[i].chances.push({
chance: outBufferToJSON.readInt(), // Chance of the unit/item (percentage)
unitIds: []
})
for (let k = 0; k < numPositions; k++) {
result.randomUnitTables[i].chances[j].unitIds.push(outBufferToJSON.readChars(4)) // unit/item id's for this line specified
}
}
}
// Struct: random item table
const numItemTable = outBufferToJSON.readInt()
for (let i = 0; i < numItemTable; i++) {
result.randomItemTables.push({
id: outBufferToJSON.readInt(), // Group number
name: outBufferToJSON.readString(), // Group name
rows: []
})
const itemSetsCurrentTable = outBufferToJSON.readInt() // Number "m" of item sets on the current item table
for (let j = 0; j < itemSetsCurrentTable; j++) {
result.randomItemTables[i].rows.push({
type: 2, // unit table (=0), a building table (=1) or an item table (=2) - not used
objects: []
})
const itemsInItemSet = outBufferToJSON.readInt() // Number "i" of items on the current item set
for (let k = 0; k < itemsInItemSet; k++) {
result.randomItemTables[i].rows[j].objects.push({
chance: outBufferToJSON.readInt(), // Percentual chance
objectId: outBufferToJSON.readChars(4) // Item id (as in ItemData.slk)
})
}
}
}
return {
errors: [],
json: result
}
}
}