ChiefOfGxBxL/WC3MapTranslator

View on GitHub
src/translators/SoundsTranslator.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { HexBuffer } from '../HexBuffer';
import { W3Buffer } from '../W3Buffer';
import { WarResult, JsonResult } from '../CommonInterfaces'

interface Sound {
    name: string;
    variableName: string;
    path: string;
    eax: string;
    flags: SoundFlags;
    fadeRate: FadeRate;
    volume: number;
    pitch: number;
    channel: number;
    distance: Distance;
}

interface FadeRate {
    in: number;
    out: number;
}

interface SoundFlags {
    looping: boolean;    // 0x00000001=looping
    '3dSound': boolean;   // 0x00000002=3D sound
    stopOutOfRange: boolean;  // 0x00000004=stop when out of range
    music: boolean;  // 0x00000008=music
}

interface Distance {
    min: number;
    max: number;
    cutoff: number;
}

export abstract class SoundsTranslator {

    public static jsonToWar(soundsJson: Sound[]): WarResult {
        const outBufferToWar = new HexBuffer();

        /*
         * Header
         */
        outBufferToWar.addInt(3); // file version
        outBufferToWar.addInt(soundsJson.length); // number of sounds

        /*
         * Body
         */
        soundsJson.forEach((sound) => {
            outBufferToWar.addString(sound.name); // e.g. gg_snd_HumanGlueScreenLoop1
            outBufferToWar.addString(sound.path); // e.g. Sound\Ambient\HumanGlueScreenLoop1.wav

            // EAX effects enum (e.g. missiles, speech, etc)
            /*
                default = DefaultEAXON
                combat = CombatSoundsEAX
                drums = KotoDrumsEAX
                spells = SpellsEAX
                missiles = MissilesEAX
                hero speech = HeroAcksEAX
                doodads = DoodadsEAX
            */
            outBufferToWar.addString(sound.eax || 'DefaultEAXON'); // defaults to "DefaultEAXON"

            // Flags, if present (optional)
            let flags = 0;
            if (sound.flags) {
                if (sound.flags.looping) flags |= 0x1;
                if (sound.flags['3dSound']) flags |= 0x2;
                if (sound.flags.stopOutOfRange) flags |= 0x4;
                if (sound.flags.music) flags |= 0x8;
            }
            outBufferToWar.addInt(flags);

            // Fade in and out rate (optional)
            outBufferToWar.addInt(sound.fadeRate ? sound.fadeRate.in || 10 : 10); // default to 10
            outBufferToWar.addInt(sound.fadeRate ? sound.fadeRate.out || 10 : 10); // default to 10

            // Volume (optional)
            outBufferToWar.addInt(sound.volume || -1); // default to -1 (for normal volume)

            // Pitch (optional)
            outBufferToWar.addFloat(sound.pitch || 1.0); // default to 1.0 for normal pitch

            // Mystery numbers... their use is unknown by the w3x documentation, but they must be present
            outBufferToWar.addFloat(0);
            outBufferToWar.addInt(8); // or -1?

            // Which channel to use? Use the lookup table for more details (optional)
            /*
                0=General
                1=Unit Selection
                2=Unit Acknowledgement
                3=Unit Movement
                4=Unit Ready
                5=Combat
                6=Error
                7=Music
                8=User Interface
                9=Looping Movement
                10=Looping Ambient
                11=Animations
                12=Constructions
                13=Birth
                14=Fire
            */
            outBufferToWar.addInt(sound.channel || 0); // default to 0

            // Distance fields
            outBufferToWar.addFloat(sound.distance.min);
            outBufferToWar.addFloat(sound.distance.max);
            outBufferToWar.addFloat(sound.distance.cutoff);

            // More mystery numbers...
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(127); // or -1?
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(0);

            outBufferToWar.addString(sound.variableName);
            outBufferToWar.addString('');
            outBufferToWar.addString(sound.path);

            // More unknowns
            outBufferToWar.addFloat(0);
            outBufferToWar.addByte(0);
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(0);
            outBufferToWar.addFloat(0);
            outBufferToWar.addByte(0);
            outBufferToWar.addFloat(0);
        });

        return {
            errors: [],
            buffer: outBufferToWar.getBuffer()
        };
    }

    public static warToJson(buffer: Buffer): JsonResult<Sound[]> {
        const result = [];
        const outBufferToJSON = new W3Buffer(buffer);

        const fileVersion = outBufferToJSON.readInt(), // File version
            numSounds = outBufferToJSON.readInt(); // # of sounds

        for (let i = 0; i < numSounds; i++) {
            const sound: Sound = {
                name: '',
                variableName: '',
                path: '',
                eax: '',
                volume: 0,
                pitch: 0,
                channel: 0,
                flags: {
                    'looping': true,    // 0x00000001=looping
                    '3dSound': true,   // 0x00000002=3D sound
                    'stopOutOfRange': true,  // 0x00000004=stop when out of range
                    'music': true  // 0x00000008=music},
                },
                fadeRate: {
                    in: 0,
                    out: 0
                },
                distance: {
                    min: 0,
                    max: 0,
                    cutoff: 0
                }
            };

            sound.name = outBufferToJSON.readString();
            sound.path = outBufferToJSON.readString();
            sound.eax = outBufferToJSON.readString();

            const flags = outBufferToJSON.readInt();
            sound.flags = {
                'looping': !!(flags & 0b1),    // 0x00000001=looping
                '3dSound': !!(flags & 0b10),   // 0x00000002=3D sound
                'stopOutOfRange': !!(flags & 0b100),  // 0x00000004=stop when out of range
                'music': !!(flags & 0b1000)  // 0x00000008=music
            };

            sound.fadeRate = {
                in: outBufferToJSON.readInt(),
                out: outBufferToJSON.readInt()
            };

            sound.volume = outBufferToJSON.readInt();
            sound.pitch = outBufferToJSON.readFloat();

            // Unknown values
            outBufferToJSON.readFloat();
            outBufferToJSON.readInt();

            sound.channel = outBufferToJSON.readInt();

            sound.distance = {
                min: outBufferToJSON.readFloat(),
                max: outBufferToJSON.readFloat(),
                cutoff: outBufferToJSON.readFloat()
            };

            // Unknown values
            outBufferToJSON.readFloat();
            outBufferToJSON.readFloat();
            outBufferToJSON.readFloat();
            outBufferToJSON.readFloat();
            outBufferToJSON.readFloat();
            outBufferToJSON.readFloat();

            sound.variableName = outBufferToJSON.readString();

            // Unknown values
            outBufferToJSON.readString();
            outBufferToJSON.readString();
            outBufferToJSON.readChars(4);
            outBufferToJSON.readChars(1);
            outBufferToJSON.readChars(4);
            outBufferToJSON.readChars(4);
            outBufferToJSON.readChars(4);
            outBufferToJSON.readChars(1);
            outBufferToJSON.readChars(4);

            result.push(sound);
        }

        return {
            errors: [],
            json: result
        };
    }
}