patchwork-mapconverter
Version:
Executable wrapper for https://github.com/ChiefOfGxBxL/WC3MapTranslator
225 lines (187 loc) • 8.63 kB
text/typescript
import { HexBuffer } from '../HexBuffer'
import { W3Buffer } from '../W3Buffer'
import { type WarResult, type JsonResult } from '../CommonInterfaces'
import { type Translator } from './Translator'
import { type ObjectModificationTable, ObjectType, TableType, type Modification, ModificationType } from '../data/ObjectModificationTable'
export class ObjectsTranslator implements Translator<ObjectModificationTable> {
private static readonly instances = new Map<ObjectType, ObjectsTranslator>()
private readonly type: ObjectType
private constructor (type: ObjectType) {
this.type = type
}
public static getInstance (type: ObjectType): ObjectsTranslator {
let instance = this.instances.get(type)
if (instance == null) {
instance = new this(type)
this.instances.set(type, instance)
}
return instance
}
public static jsonToWar (type: ObjectType, info: ObjectModificationTable): WarResult {
return this.getInstance(type).jsonToWar(info)
}
public static warToJson (type: ObjectType, buffer: Buffer): JsonResult<ObjectModificationTable> {
return this.getInstance(type).warToJson(buffer)
}
// Expose the ObjectType enum as part of this abstract class
// The enum could be "export"ed , but it wouldn't be accessible
// via `ObjectsTranslator.ObjectType`, which is preferable.
public static readonly ObjectType = ObjectType
private static readonly varTypes = {
int: 0,
real: 1,
unreal: 2,
string: 3,
0: 'int',
1: 'real',
2: 'unreal',
3: 'string'
}
public jsonToWar (json: ObjectModificationTable): WarResult {
const outBufferToWar = new HexBuffer()
/*
* Header
*/
outBufferToWar.addInt(2) // file version
const generateTableFromJson = (tableType: TableType, tableData: object): void => { // create "original" or "custom" table
if (!tableData) {
outBufferToWar.addInt(0)
return
}
const data = Object.entries(tableData)
outBufferToWar.addInt(data.length)
data.forEach(([defKey, obj]) => {
// Original and new object ids
if (tableType === TableType.original) {
outBufferToWar.addChars(defKey)
outBufferToWar.addByte(0); outBufferToWar.addByte(0); outBufferToWar.addByte(0); outBufferToWar.addByte(0) // no new Id is assigned
} else {
// e.g. "h000:hfoo"
outBufferToWar.addChars(defKey.substring(5, 9)) // original id
outBufferToWar.addChars(defKey.substring(0, 4)) // custom id
}
// Number of modifications made to this object
outBufferToWar.addInt(obj?.length || 0)
obj?.forEach((mod: Modification) => {
let modType: number
// Modification id (e.g. unam = name; reference MetaData lookups)
outBufferToWar.addChars(mod.id)
// Determine what type of field the mod is (int, real, unreal, string)
if (mod.type != null) { // if a type is specified, use it
modType = ObjectsTranslator.varTypes[mod.type]
} else { // otherwise we try to infer between int/string (note there is no way to detect unreal or float this way, so user must specify those explicitly)
if (typeof mod.value === 'number') {
modType = ObjectsTranslator.varTypes.int
} else if (typeof mod.value === 'string') {
modType = ObjectsTranslator.varTypes.string
} else {
throw new Error('No type specified and cannot infer type!')
}
}
outBufferToWar.addInt(modType)
// Addl integers
// Required for: doodads, abilities, upgrades
if (this.type === ObjectType.Doodads || this.type === ObjectType.Abilities || this.type === ObjectType.Upgrades) {
// Level or variation
// We need to check if hasOwnProperty because these could be explititly
// set to 0, but JavaScript's truthiness evaluates to false to it was defaulting
outBufferToWar.addInt((mod.level ?? mod.variation) ?? 0) // defaults to 0
outBufferToWar.addInt(mod.column ?? 0) // E.g DataA1 is 1 because of col A; refer to the xyzData.slk files for Data fields
}
// Write mod value
if (modType === ObjectsTranslator.varTypes.int) {
outBufferToWar.addInt(mod.value as number)
} else if (modType === ObjectsTranslator.varTypes.real || modType === ObjectsTranslator.varTypes.unreal) {
// Follow-up: check if unreal values are same hex format as real
outBufferToWar.addFloat(mod.value as number)
} else if (modType === ObjectsTranslator.varTypes.string) {
// Note that World Editor normally creates a TRIGSTR_000 for these string
// values - WC3MapTranslator just writes the string directly to file
outBufferToWar.addString(mod.value as string)
}
// End of struct
if (tableType === TableType.original) {
// Original objects are ended with their base id (e.g. hfoo)
outBufferToWar.addChars(defKey)
} else {
// Custom objects are ended with 0000 bytes
outBufferToWar.addByte(0)
outBufferToWar.addByte(0)
outBufferToWar.addByte(0)
outBufferToWar.addByte(0)
}
})
})
}
generateTableFromJson(TableType.original, json.original)
generateTableFromJson(TableType.custom, json.custom)
return {
errors: [],
buffer: outBufferToWar.getBuffer()
}
}
public warToJson (buffer: Buffer): JsonResult<ObjectModificationTable> {
const result: ObjectModificationTable = { original: {}, custom: {} }
const outBufferToJSON = new W3Buffer(buffer)
const fileVersion = outBufferToJSON.readInt()
const readModificationTable = (isOriginalTable: boolean): void => {
const numTableModifications = outBufferToJSON.readInt()
for (let i = 0; i < numTableModifications; i++) {
const objectDefinition: Modification[] = [] // object definition will store one or more modification objects
const originalId = outBufferToJSON.readChars(4)
const customId = outBufferToJSON.readChars(4)
let sets: number
if (fileVersion >= 3) {
sets = outBufferToJSON.readInt()
} else {
sets = 1
}
for (let j = 0; j < sets; j++) {
if (fileVersion >= 3) {
const setFlag = outBufferToJSON.readInt()
}
const modificationCount = outBufferToJSON.readInt()
for (let k = 0; k < modificationCount; k++) {
const modification: Modification = {
id: '',
type: ModificationType.string,
level: 0,
column: 0,
value: {}
}
modification.id = outBufferToJSON.readChars(4)
modification.type = ObjectsTranslator.varTypes[outBufferToJSON.readInt()] as unknown as ModificationType // 'int' | 'real' | 'unreal' | 'string',
if (this.type === ObjectType.Doodads || this.type === ObjectType.Abilities || this.type === ObjectType.Upgrades) {
modification.level = outBufferToJSON.readInt()
modification.column = outBufferToJSON.readInt()
}
if (modification.type === 'int') {
modification.value = outBufferToJSON.readInt()
} else if (modification.type === 'real' || modification.type === 'unreal') {
modification.value = outBufferToJSON.readFloat()
} else { // modification.type === 'string'
modification.value = outBufferToJSON.readString()
}
if (isOriginalTable) {
outBufferToJSON.readInt() // should be 0 for original objects
} else {
outBufferToJSON.readChars(4) // should be object ID for custom objects
}
objectDefinition.push(modification)
}
}
if (isOriginalTable) {
result.original[originalId] = objectDefinition
} else {
result.custom[customId + ':' + originalId] = objectDefinition
}
}
}
readModificationTable(true)
readModificationTable(false)
return {
errors: [],
json: result
}
}
}