Guest User

revenant-map-builder.js

a guest
Nov 9th, 2024
58
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 98.58 KB | Gaming | 0 0
  1. const fs = require('fs').promises;
  2. const path = require("path");
  3.  
  4. class InputStream {
  5.     constructor(arrayBuffer) {
  6.         this.dataView = new DataView(arrayBuffer);
  7.         this.offset = 0;
  8.     }
  9.  
  10.     checkBounds(bytesToRead) {
  11.         if (this.offset + bytesToRead > this.dataView.byteLength) {
  12.             throw new EOFError();
  13.         }
  14.     }
  15.  
  16.     readInt32() {
  17.         this.checkBounds(4);
  18.         const value = this.dataView.getInt32(this.offset, true);
  19.         this.offset += 4;
  20.         return value;
  21.     }
  22.  
  23.     readInt16() {
  24.         this.checkBounds(2);
  25.         const value = this.dataView.getInt16(this.offset, true);
  26.         this.offset += 2;
  27.         return value;
  28.     }
  29.  
  30.     readUint32() {
  31.         this.checkBounds(4);
  32.         const value = this.dataView.getUint32(this.offset, true);
  33.         this.offset += 4;
  34.         return value;
  35.     }
  36.  
  37.     readUint16() {
  38.         this.checkBounds(2);
  39.         const value = this.dataView.getUint16(this.offset, true);
  40.         this.offset += 2;
  41.         return value;
  42.     }
  43.  
  44.     readUint8() {
  45.         this.checkBounds(1);
  46.         const value = this.dataView.getUint8(this.offset);
  47.         this.offset += 1;
  48.         return value;
  49.     }
  50.  
  51.     readString() {
  52.         const length = this.readUint8();
  53.         this.checkBounds(length);
  54.         const bytes = new Uint8Array(this.dataView.buffer, this.dataView.byteOffset + this.offset, length);
  55.         this.offset += length;
  56.         return new TextDecoder('ascii').decode(bytes);
  57.     }
  58.  
  59.     skip(bytes) {
  60.         this.checkBounds(bytes);
  61.         this.offset += bytes;
  62.     }
  63.  
  64.     setPos(pos) {
  65.         this.offset = pos;
  66.     }
  67.  
  68.     getPos() {
  69.         return this.offset;
  70.     }
  71.  
  72.     eof() {
  73.         return this.offset >= this.dataView.byteLength;
  74.     }
  75. }
  76.  
  77. class EOFError extends Error {
  78.     constructor(message = "End of file reached") {
  79.         super(message);
  80.         this.name = "EOFError";
  81.     }
  82. }
  83.  
  84. class BufferUtils {
  85.     /**
  86.      * Creates a new ArrayBuffer and Stream from a slice of an existing buffer
  87.      * @param {ArrayBuffer} sourceBuffer - The source ArrayBuffer to slice from
  88.      * @param {number} offset - The starting offset in the source buffer
  89.      * @param {number} length - The length of data to copy
  90.      * @returns {{buffer: ArrayBuffer, stream: InputStream}} Object containing the new buffer and stream
  91.      */
  92.     static createBufferSlice(sourceBuffer, offset, length) {
  93.         // Create a new buffer of the specified length
  94.         const newBuffer = new ArrayBuffer(length);
  95.         const newArray = new Uint8Array(newBuffer);
  96.  
  97.         // Copy the data from the source buffer
  98.         const sourceArray = new Uint8Array(sourceBuffer, offset, length);
  99.         newArray.set(sourceArray);
  100.  
  101.         // Create and return a new stream for the buffer
  102.         const stream = new InputStream(newBuffer);
  103.  
  104.         return {
  105.             buffer: newBuffer,
  106.             stream: stream
  107.         };
  108.     }
  109. }
  110.  
  111. class ImageryDatParser {
  112.     static QUICKLOAD_FILE_ID = ('H'.charCodeAt(0)) |
  113.         ('D'.charCodeAt(0) << 8) |
  114.         ('R'.charCodeAt(0) << 16) |
  115.         ('S'.charCodeAt(0) << 24);
  116.     static QUICKLOAD_FILE_VERSION = 1;
  117.     static MAX_IMAGERY_FILENAME_LENGTH = 80; // MAXIMFNAMELEN
  118.     static MAXANIMNAME = 32;
  119.     static gameDir = "";
  120.  
  121.     static async loadFile(filePath, gameDir) {
  122.         try {
  123.             this.gameDir = gameDir;
  124.             const buffer = await fs.readFile(filePath);
  125.             const arrayBuffer = buffer.buffer.slice(
  126.                 buffer.byteOffset,
  127.                 buffer.byteOffset + buffer.byteLength
  128.             );
  129.             return await ImageryDatParser.parse(arrayBuffer);
  130.         } catch (error) {
  131.             console.error('Error loading IMAGERY.DAT file:', error);
  132.             debugger;
  133.             return null;
  134.         }
  135.     }
  136.  
  137.     static async parse(arrayBuffer) {
  138.         const stream = new InputStream(arrayBuffer);
  139.  
  140.         // Read QuickLoad Header
  141.         const header = {
  142.             id: stream.readUint32(),
  143.             version: stream.readUint32(),
  144.             numHeaders: stream.readUint32()
  145.         };
  146.  
  147.         // Validate header
  148.         if (header.id !== this.QUICKLOAD_FILE_ID) {
  149.             throw new Error('Invalid IMAGERY.DAT file ID');
  150.         }
  151.  
  152.         if (header.version !== this.QUICKLOAD_FILE_VERSION) {
  153.             throw new Error('Unsupported IMAGERY.DAT version');
  154.         }
  155.  
  156.         // Read imagery entries
  157.         const entries = [];
  158.         for (let i = 0; i < header.numHeaders; i++) {
  159.             // Read filename (fixed-length buffer)
  160.             const filenameBytes = new Uint8Array(arrayBuffer, stream.getPos(), this.MAX_IMAGERY_FILENAME_LENGTH);
  161.             stream.skip(this.MAX_IMAGERY_FILENAME_LENGTH);
  162.  
  163.             // Convert to string until first null terminator
  164.             let filename = '';
  165.             for (let j = 0; j < filenameBytes.length; j++) {
  166.                 if (filenameBytes[j] === 0) break;
  167.                 filename += String.fromCharCode(filenameBytes[j]);
  168.             }
  169.  
  170.             // Read header size
  171.             const headerSize = stream.readUint32();
  172.  
  173.             // Read imagery header
  174.             const imageryHeader = this.parseImageryHeader(stream, headerSize, arrayBuffer);
  175.  
  176.             // Read imagery body
  177.             const imageryBody = await this.parseImageryBody(filename, imageryHeader);
  178.  
  179.             // Add entry to the list
  180.             entries.push({
  181.                 filename,
  182.                 headerSize,
  183.                 header: imageryHeader,
  184.                 body: imageryBody
  185.             });
  186.         }
  187.  
  188.         return {
  189.             header,
  190.             entries
  191.         };
  192.     }
  193.  
  194.     static parseImageryHeader(stream, headerSize, arrayBuffer) {
  195.         const startPos = stream.getPos();
  196.  
  197.         const header = {
  198.             imageryId: ImageryType.getName(stream.readUint32()),    // Id number for imagery handler (index to builder array)
  199.             numStates: stream.readInt32(),    // Number of states
  200.             states: []
  201.         };
  202.  
  203.         // Read state headers
  204.         for (let i = 0; i < header.numStates; i++) {
  205.             // Read animation name first
  206.             const animNameBytes = new Uint8Array(arrayBuffer, stream.getPos(), this.MAXANIMNAME);
  207.             stream.skip(this.MAXANIMNAME);
  208.  
  209.             let animName = '';
  210.             for (let j = 0; j < animNameBytes.length; j++) {
  211.                 if (animNameBytes[j] === 0) break;
  212.                 animName += String.fromCharCode(animNameBytes[j]);
  213.             }
  214.  
  215.             const state = {
  216.                 animName,                    // Array of Ascii Names
  217.                 walkMap: stream.readUint32(), // Walkmap (OFFSET type)
  218.                 flags: new ObjectFlags(stream.readUint32()),   // Imagery state flags (DWORD)
  219.                 aniFlags: new AnimationFlags(stream.readInt16()), // Animation state flags (short)
  220.                 frames: stream.readInt16(),   // Number of frames (short)
  221.                 width: stream.readInt16(),    // Graphics maximum width (short)
  222.                 height: stream.readInt16(),   // Graphics maximum height (short)
  223.                 regX: stream.readInt16(),     // Registration point x for graphics (short)
  224.                 regY: stream.readInt16(),     // Registration point y for graphics (short)
  225.                 regZ: stream.readInt16(),     // Registration point z for graphics (short)
  226.                 animRegX: stream.readInt16(), // Registration point of animation x (short)
  227.                 animRegY: stream.readInt16(), // Registration point of animation y (short)
  228.                 animRegZ: stream.readInt16(), // Registration point of animation z (short)
  229.                 wRegX: stream.readInt16(),    // World registration x of walk and bounding box (short)
  230.                 wRegY: stream.readInt16(),    // World registration y of walk and bounding box (short)
  231.                 wRegZ: stream.readInt16(),    // World registration z of walk and bounding box (short)
  232.                 wWidth: stream.readInt16(),   // Object's world width for walk map and bound box (short)
  233.                 wLength: stream.readInt16(),  // Object's world length for walk map and bound box (short)
  234.                 wHeight: stream.readInt16(),  // Object's world height for walk map and bound box (short)
  235.                 invAniFlags: new AnimationFlags(stream.readInt16()), // Animation flags for inventory animation (short)
  236.                 invFrames: stream.readInt16()    // Number of frames of inventory animation (short)
  237.             };
  238.  
  239.             header.states.push(state);
  240.         }
  241.  
  242.         // Ensure we've read exactly headerSize bytes
  243.         const bytesRead = stream.getPos() - startPos;
  244.         if (bytesRead < headerSize) {
  245.             stream.skip(headerSize - bytesRead);
  246.         }
  247.  
  248.         return header;
  249.     }
  250.  
  251.     static async parseImageryBody(filename, imageryHeader) {
  252.         // The imagery body is actually stored in separate .I2D or .I3D files
  253.         // We need to load these files using the CGSResourceParser
  254.  
  255.         // First, determine if it's a 2D or 3D imagery based on the filename extension
  256.         const extension = path.extname(filename).toLowerCase();
  257.         const is3D = extension === '.i3d';
  258.  
  259.         // Create a result object to store all imagery data
  260.         const result = {
  261.             filename,
  262.             header: imageryHeader,
  263.             is3D,
  264.             states: []
  265.         };
  266.  
  267.         // For each state in the imagery header, load its corresponding resource file
  268.         for (let i = 0; i < imageryHeader.numStates; i++) {
  269.             const state = imageryHeader.states[i];
  270.  
  271.             // Construct the resource filename
  272.             // The resource files are typically stored in the Imagery directory
  273.             const resourcePath = filename;
  274.  
  275.             try {
  276.                 // Load the resource file
  277.                 const resource = await DatParser.loadResourceFile(this.gameDir, resourcePath);
  278.  
  279.                 if (resource) {
  280.                     // Add the loaded resource data to our state
  281.                     result.states.push({
  282.                         ...state,
  283.                         resource: resource,
  284.                         bitmaps: resource.bitmaps.map(bitmap => ({
  285.                             ...bitmap,
  286.                         }))
  287.                     });
  288.                 } else {
  289.                     console.warn(`Failed to load resource for state ${i} of ${filename}`);
  290.                     result.states.push({
  291.                         ...state,
  292.                         resource: null,
  293.                         bitmaps: []
  294.                     });
  295.                 }
  296.             } catch (error) {
  297.                 console.error(`Error loading resource for state ${i} of ${filename}:`, error);
  298.                 result.states.push({
  299.                     ...state,
  300.                     resource: null,
  301.                     bitmaps: []
  302.                 });
  303.             }
  304.         }
  305.  
  306.         return result;
  307.     }
  308. }
  309.  
  310. class ImageryType {
  311.     static ANIMATION = 0;
  312.     static MESH3D = 1;
  313.     static MESH3DHELPER = 2;
  314.     static MULTI = 3;
  315.     static MULTIANIMATION = 4;
  316.  
  317.     static getName(id) {
  318.         switch (id) {
  319.             case this.ANIMATION: return 'ANIMATION';
  320.             case this.MESH3D: return 'MESH3D';
  321.             case this.MESH3DHELPER: return 'MESH3DHELPER';
  322.             case this.MULTI: return 'MULTI';
  323.             case this.MULTIANIMATION: return 'MULTIANIMATION';
  324.             default: return 'UNKNOWN';
  325.         }
  326.     }
  327.  
  328.     static isValid(id) {
  329.         return id >= this.ANIMATION && id <= this.MULTIANIMATION;
  330.     }
  331. }
  332.  
  333. class AnimationFlags {
  334.     constructor(value) {
  335.         // Convert number to 16-bit binary string (animation flags are 16-bit)
  336.         const bits = (value >>> 0).toString(2).padStart(16, '0');
  337.  
  338.         // Parse all animation flags
  339.         this.looping = !!parseInt(bits[15 - 0]);         // Circles back to original position
  340.         this.faceMotion = !!parseInt(bits[15 - 1]);      // This animation has facing motion data
  341.         this.interFrame = !!parseInt(bits[15 - 2]);      // Uses interframe compression
  342.         this.noReg = !!parseInt(bits[15 - 3]);           // Use 0,0 of FLC as registration pt of animation
  343.         this.synchronize = !!parseInt(bits[15 - 4]);     // Causes animation frame to match 'targets' animation frame
  344.         this.move = !!parseInt(bits[15 - 5]);            // Use the deltas in the ani to move the x,y position
  345.         this.noInterpolation = !!parseInt(bits[15 - 6]); // Prevents system from interpolating between animations
  346.         this.pingPong = !!parseInt(bits[15 - 7]);        // Pingpong the animation
  347.         this.reverse = !!parseInt(bits[15 - 8]);         // Play the animation backwards
  348.         this.noRestore = !!parseInt(bits[15 - 9]);       // Draw to screen but don't bother to restore
  349.         this.root = !!parseInt(bits[15 - 10]);           // This animation is a root state
  350.         this.fly = !!parseInt(bits[15 - 11]);            // This animation is a flying animation (jump, etc.)
  351.         this.sync = !!parseInt(bits[15 - 12]);           // Synchronize all animations on screen
  352.         this.noMotion = !!parseInt(bits[15 - 13]);       // Ignore motion deltas
  353.         this.accurateKeys = !!parseInt(bits[15 - 14]);   // Has only high accuracy 'code' style 3D ani keys
  354.         this.root2Root = !!parseInt(bits[15 - 15]);      // This animation starts at root and returns to root
  355.     }
  356.  
  357.     // Helper method to check if animation is a root animation
  358.     isRootAnimation() {
  359.         return this.root || this.root2Root;
  360.     }
  361.  
  362.     // Helper method to check if animation involves motion
  363.     hasMotion() {
  364.         return this.move && !this.noMotion;
  365.     }
  366.  
  367.     // Helper method to check if animation needs synchronization
  368.     needsSync() {
  369.         return this.synchronize || this.sync;
  370.     }
  371.  
  372.     // Helper method to check if animation loops in some way
  373.     isLooping() {
  374.         return this.looping || this.pingPong;
  375.     }
  376.  
  377.     // Helper method to get playback direction
  378.     getPlaybackDirection() {
  379.         if (this.pingPong) return 'pingpong';
  380.         if (this.reverse) return 'reverse';
  381.         return 'forward';
  382.     }
  383. }
  384.  
  385. class BitmapFlags {
  386.     constructor(value) {
  387.         // Convert number to 32-bit binary string
  388.         const bits = (value >>> 0).toString(2).padStart(32, '0');
  389.  
  390.         // Bit depth flags
  391.         this.bm_8bit = !!parseInt(bits[31 - 0]);     // Bitmap data is 8 bit
  392.         this.bm_15bit = !!parseInt(bits[31 - 1]);    // Bitmap data is 15 bit
  393.         this.bm_16bit = !!parseInt(bits[31 - 2]);    // Bitmap data is 16 bit
  394.         this.bm_24bit = !!parseInt(bits[31 - 3]);    // Bitmap data is 24 bit
  395.         this.bm_32bit = !!parseInt(bits[31 - 4]);    // Bitmap data is 32 bit
  396.  
  397.         // Buffer flags
  398.         this.bm_zbuffer = !!parseInt(bits[31 - 5]);  // Bitmap has ZBuffer
  399.         this.bm_normals = !!parseInt(bits[31 - 6]);  // Bitmap has Normal Buffer
  400.         this.bm_alias = !!parseInt(bits[31 - 7]);    // Bitmap has Alias Buffer
  401.         this.bm_alpha = !!parseInt(bits[31 - 8]);    // Bitmap has Alpha Buffer
  402.         this.bm_palette = !!parseInt(bits[31 - 9]);  // Bitmap has 256 Color SPalette Structure
  403.  
  404.         // Special flags
  405.         this.bm_regpoint = !!parseInt(bits[31 - 10]);    // Bitmap has registration point
  406.         this.bm_nobitmap = !!parseInt(bits[31 - 11]);    // Bitmap has no pixel data
  407.         this.bm_5bitpal = !!parseInt(bits[31 - 12]);     // Bitmap palette is 5 bit for r,g,b instead of 8 bit
  408.         this.bm_compressed = !!parseInt(bits[31 - 14]);  // Bitmap is compressed
  409.         this.bm_chunked = !!parseInt(bits[31 - 15]);     // Bitmap is chunked out
  410.     }
  411.  
  412.     // Helper methods to check bit depth
  413.     getBitDepth() {
  414.         if (this.bm_8bit) return 8;
  415.         if (this.bm_15bit) return 15;
  416.         if (this.bm_16bit) return 16;
  417.         if (this.bm_24bit) return 24;
  418.         if (this.bm_32bit) return 32;
  419.         return 0;
  420.     }
  421.  
  422.     // Helper method to get bytes per pixel
  423.     getBytesPerPixel() {
  424.         if (this.bm_8bit) return 1;
  425.         if (this.bm_15bit || this.bm_16bit) return 2;
  426.         if (this.bm_24bit) return 3;
  427.         if (this.bm_32bit) return 4;
  428.         return 0;
  429.     }
  430.  
  431.     // Helper method to check if bitmap needs palette
  432.     needsPalette() {
  433.         return this.bm_8bit && this.bm_palette;
  434.     }
  435.  
  436.     // Helper method to check if bitmap is valid
  437.     isValid() {
  438.         // Check that only one bit depth flag is set
  439.         const bitDepthFlags = [
  440.             this.bm_8bit,
  441.             this.bm_15bit,
  442.             this.bm_16bit,
  443.             this.bm_24bit,
  444.             this.bm_32bit
  445.         ].filter(flag => flag).length;
  446.  
  447.         return bitDepthFlags === 1 || this.bm_nobitmap;
  448.     }
  449. }
  450.  
  451. class DrawModeFlags {
  452.     constructor(value) {
  453.         // Convert number to 32-bit binary string
  454.         const bits = (value >>> 0).toString(2).padStart(32, '0');
  455.  
  456.         // Clipping flags
  457.         this.dm_noclip = !!parseInt(bits[31 - 0]);       // Disables clipping when drawing
  458.         this.dm_wrapclip = !!parseInt(bits[31 - 1]);     // Enables wrap clipping
  459.         this.dm_wrapclipsrc = !!parseInt(bits[31 - 2]);  // Enables wrap clipping of source buffer
  460.         this.dm_nowrapclip = !!parseInt(bits[31 - 26]);  // Overrides surface clipping mode to not wrap clip
  461.  
  462.         // Drawing mode flags
  463.         this.dm_stretch = !!parseInt(bits[31 - 3]);      // Enables Stretching when drawing
  464.         this.dm_background = !!parseInt(bits[31 - 4]);    // Draws bitmap to background
  465.         this.dm_norestore = !!parseInt(bits[31 - 5]);    // Disables automatic background restoring
  466.         this.dm_fill = !!parseInt(bits[31 - 29]);        // Fills the destination with the current color
  467.  
  468.         // Orientation flags
  469.         this.dm_reversevert = !!parseInt(bits[31 - 6]);  // Reverses vertical orientation
  470.         this.dm_reversehorz = !!parseInt(bits[31 - 7]);  // Reverses horizontal orientation
  471.  
  472.         // Transparency and effects flags
  473.         this.dm_transparent = !!parseInt(bits[31 - 8]);   // Enables transparent drawing
  474.         this.dm_shutter = !!parseInt(bits[31 - 14]);     // Enable Shutter transparent drawing
  475.         this.dm_translucent = !!parseInt(bits[31 - 15]); // Enables Translucent drawing
  476.         this.dm_fade = !!parseInt(bits[31 - 16]);        // Fade image to key color
  477.  
  478.         // Buffer flags
  479.         this.dm_zmask = !!parseInt(bits[31 - 9]);        // Enables ZBuffer Masking
  480.         this.dm_zbuffer = !!parseInt(bits[31 - 10]);     // Draws bitmap ZBuffer to destination ZBuffer
  481.         this.dm_normals = !!parseInt(bits[31 - 11]);     // Draws bitmap Normals to dest. Normal buffer
  482.         this.dm_zstatic = !!parseInt(bits[31 - 23]);     // Draws bitmap at a static z value
  483.         this.dm_nocheckz = !!parseInt(bits[31 - 28]);    // Causes ZBuffer Draws to use transparency only
  484.  
  485.         // Enhancement flags
  486.         this.dm_alias = !!parseInt(bits[31 - 12]);       // Antiailiases edges using bitmap alias data
  487.         this.dm_alpha = !!parseInt(bits[31 - 13]);       // Enables Alpha drawing
  488.         this.dm_alphalighten = !!parseInt(bits[31 - 24]);// Enables Alpha drawing lighten only
  489.  
  490.         // Color modification flags
  491.         this.dm_changecolor = !!parseInt(bits[31 - 19]); // Draw in a different color
  492.         this.dm_changehue = !!parseInt(bits[31 - 20]);   // Use color to modify hue of image
  493.         this.dm_changesv = !!parseInt(bits[31 - 21]);    // Use color to modify saturation and brightness
  494.  
  495.         // Special flags
  496.         this.dm_usereg = !!parseInt(bits[31 - 17]);      // Draws bitmap based on registration point
  497.         this.dm_selected = !!parseInt(bits[31 - 18]);    // Draw selection highlight around bitmap
  498.         this.dm_nodraw = !!parseInt(bits[31 - 22]);      // Prevents bitmap graphics buffer from drawing
  499.         this.dm_doescallback = !!parseInt(bits[31 - 25]);// Flag set by low level draw func
  500.         this.dm_nohardware = !!parseInt(bits[31 - 27]);  // Force no hardware use
  501.         this.dm_usedefault = !!parseInt(bits[31 - 31]);  // Causes draw routines to supply default value
  502.     }
  503.  
  504.     // Helper methods
  505.     isDefault() {
  506.         return this.value === 0;
  507.     }
  508.  
  509.     hasTransparencyEffect() {
  510.         return this.dm_transparent || this.dm_translucent ||
  511.             this.dm_shutter || this.dm_alpha ||
  512.             this.dm_alphalighten;
  513.     }
  514.  
  515.     hasColorModification() {
  516.         return this.dm_changecolor || this.dm_changehue ||
  517.             this.dm_changesv;
  518.     }
  519.  
  520.     isFlipped() {
  521.         return this.dm_reversevert || this.dm_reversehorz;
  522.     }
  523.  
  524.     usesZBuffer() {
  525.         return this.dm_zmask || this.dm_zbuffer ||
  526.             this.dm_zstatic || !this.dm_nocheckz;
  527.     }
  528.  
  529.     toString() {
  530.         const flags = [];
  531.         for (const [key, value] of Object.entries(this)) {
  532.             if (value === true) {
  533.                 flags.push(key);
  534.             }
  535.         }
  536.         return flags.join(' | ') || 'DM_DEFAULT';
  537.     }
  538. }
  539.  
  540. class ChunkHeader {
  541.     constructor(stream) {
  542.         // Read the fixed part of the header
  543.         this.type = stream.readUint32();      // Compressed flag
  544.         this.width = stream.readInt32();      // Width in blocks
  545.         this.height = stream.readInt32();     // Height in blocks
  546.  
  547.         // Validate dimensions
  548.         if (this.width <= 0 || this.height <= 0 ||
  549.             this.width > 128 || this.height > 128) {
  550.             throw new Error(`Invalid chunk header dimensions: ${this.width}x${this.height}`);
  551.         }
  552.  
  553.         // Read the flexible array of block offsets
  554.         const numBlocks = this.width * this.height;
  555.         this.blocks = new Array(numBlocks);
  556.         for (let i = 0; i < numBlocks; i++) {
  557.             let currentOffset = stream.getPos();
  558.             let relativeOffset = stream.readUint32()
  559.             // If the offset is 0, it means the block is empty
  560.             if (relativeOffset === 0) {
  561.                 this.blocks[i] = 0;
  562.             } else {
  563.                 // Otherwise, it's a relative offset from the start of the bitmap data
  564.                 this.blocks[i] = relativeOffset + currentOffset;
  565.             }
  566.         }
  567.     }
  568.  
  569.     // Helper method to check if a block is blank
  570.     isBlockBlank(x, y) {
  571.         const blockIndex = this.getBlockIndex(x, y);
  572.         return this.blocks[blockIndex] === 0;
  573.     }
  574.  
  575.     // Helper method to get block index from x,y coordinates
  576.     getBlockIndex(x, y) {
  577.         if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
  578.             return -1;
  579.         }
  580.         return y * this.width + x;
  581.     }
  582.  
  583.     getBlockSize(x, y) {
  584.         const currentIndex = this.getBlockIndex(x, y);
  585.         if (currentIndex === -1 || this.blocks[currentIndex] === 0) {
  586.             return 0;
  587.         }
  588.  
  589.         const currentOffset = this.blocks[currentIndex];
  590.  
  591.         // Look for the next non-zero block offset
  592.         for (let i = currentIndex + 1; i < this.blocks.length; i++) {
  593.             if (this.blocks[i] !== 0) {
  594.                 return this.blocks[i] - currentOffset;
  595.             }
  596.         }
  597.  
  598.         // If we didn't find any non-zero blocks after this one,
  599.         // or if this is the last block, return 0
  600.         return 0;
  601.     }
  602.  
  603.     // Helper method to get block offset from x,y coordinates
  604.     getBlockOffset(x, y) {
  605.         const index = this.getBlockIndex(x, y);
  606.         return index >= 0 ? this.blocks[index] : 0;
  607.     }
  608. }
  609.  
  610. class ChunkDecompressor {
  611.     static decompressChunk(stream, blockWidth, blockHeight, clear = 1) {
  612.         // Get chunk number from stream
  613.         const number = stream.readUint8();
  614.         const bite1 = stream.readUint8();
  615.         const bite2 = stream.readUint8();
  616.         const bite3 = stream.readUint8();
  617.         console.log(`Decompressing chunk ${number} with flags ${bite1} ${bite2} ${bite3}`);
  618.  
  619.         // Get compression markers
  620.         const rleMarker = stream.readUint8();
  621.         const lzMarker = stream.readUint8();
  622.  
  623.         // Create destination buffer (start with expected size, but might grow)
  624.         // Create destination buffer with the correct size
  625.         let dest = new Uint8Array(blockWidth * blockHeight);
  626.         const clearValue = clear === 1 ? 0x00 : 0xFF;
  627.         dest.fill(clearValue);
  628.  
  629.         let dstPos = 0;
  630.  
  631.         try {
  632.             while (!stream.eof()) {
  633.                 const byte = stream.readUint8();
  634.  
  635.                 if (byte === rleMarker) {
  636.                     // RLE compression
  637.                     let count = stream.readUint8();
  638.  
  639.                     if (count & 0x80) {
  640.                         // Skip RLE
  641.                         count &= 0x7F;
  642.                         dstPos += count;
  643.                     } else {
  644.                         // Normal RLE
  645.                         const value = stream.readUint8();
  646.                         // Ensure dest array is large enough
  647.                         if (dstPos + count > dest.length) {
  648.                             const newDest = new Uint8Array(dest.length * 2);
  649.                             newDest.set(dest);
  650.                             dest = newDest;
  651.                         }
  652.                         for (let i = 0; i < count; i++) {
  653.                             dest[dstPos++] = value;
  654.                         }
  655.                     }
  656.                 } else if (byte === lzMarker) {
  657.                     // LZ compression
  658.                     const count = stream.readUint8();
  659.                     const offset = stream.readUint16();
  660.  
  661.                     // Ensure dest array is large enough
  662.                     if (dstPos + count > dest.length) {
  663.                         const newDest = new Uint8Array(dest.length * 2);
  664.                         newDest.set(dest);
  665.                         dest = newDest;
  666.                     }
  667.  
  668.                     // Copy from earlier in the output
  669.                     for (let i = 0; i < count; i++) {
  670.                         dest[dstPos] = dest[dstPos - offset];
  671.                         dstPos++;
  672.                     }
  673.                 } else {
  674.                     // Raw byte
  675.                     // Ensure dest array is large enough
  676.                     if (dstPos >= dest.length) {
  677.                         const newDest = new Uint8Array(dest.length * 2);
  678.                         newDest.set(dest);
  679.                         dest = newDest;
  680.                     }
  681.                     dest[dstPos++] = byte;
  682.                 }
  683.             }
  684.         } catch (e) {
  685.             if (!(e instanceof EOFError)) {
  686.                 console.error("Error decompressing chunk:", e);
  687.                 debugger;
  688.             }
  689.         }
  690.  
  691.         // Trim the array to actual size
  692.         const finalDest = new Uint8Array(dstPos);
  693.         finalDest.set(dest.subarray(0, dstPos));
  694.  
  695.         return {
  696.             number,
  697.             data: finalDest
  698.         };
  699.     }
  700. }
  701.  
  702. class BitmapData {
  703.     static readBitmap(stream, arrayBuffer) {
  704.         // Read the fixed-size header structure
  705.         const bitmap = {
  706.             width: stream.readInt32(),
  707.             height: stream.readInt32(),
  708.             regx: stream.readInt32(),
  709.             regy: stream.readInt32(),
  710.             flags: new BitmapFlags(stream.readUint32()),
  711.             drawmode: new DrawModeFlags(stream.readUint32()),
  712.             keycolor: stream.readUint32(),
  713.         };
  714.  
  715.         // Read sizes and offsets, adjusting the offsets based on their position
  716.         bitmap.aliassize = stream.readUint32();
  717.         const aliasOffsetPos = stream.getPos();
  718.         bitmap.alias = stream.readUint32();
  719.         if (bitmap.alias !== 0) {
  720.             bitmap.alias += aliasOffsetPos;
  721.         }
  722.  
  723.         bitmap.alphasize = stream.readUint32();
  724.         const alphaOffsetPos = stream.getPos();
  725.         bitmap.alpha = stream.readUint32();
  726.         if (bitmap.alpha !== 0) {
  727.             bitmap.alpha += alphaOffsetPos;
  728.         }
  729.  
  730.         bitmap.zbuffersize = stream.readUint32();
  731.         const zbufferOffsetPos = stream.getPos();
  732.         bitmap.zbuffer = stream.readUint32();
  733.         if (bitmap.zbuffer !== 0) {
  734.             bitmap.zbuffer += zbufferOffsetPos;
  735.         }
  736.  
  737.         bitmap.normalsize = stream.readUint32();
  738.         const normalOffsetPos = stream.getPos();
  739.         bitmap.normal = stream.readUint32();
  740.         if (bitmap.normal !== 0) {
  741.             bitmap.normal += normalOffsetPos;
  742.         }
  743.  
  744.         bitmap.palettesize = stream.readUint32();
  745.         const paletteOffsetPos = stream.getPos();
  746.         bitmap.palette = stream.readUint32();
  747.         if (bitmap.palette !== 0) {
  748.             bitmap.palette += paletteOffsetPos;
  749.         }
  750.  
  751.         bitmap.datasize = stream.readUint32();
  752.  
  753.         // Sanity checks
  754.         if (bitmap.width > 8192 || bitmap.height > 8192) {
  755.             throw new Error('Corrupted bitmap dimensions');
  756.         }
  757.  
  758.         if (!bitmap.flags.isValid()) {
  759.             throw new Error('Invalid bitmap flags configuration');
  760.         }
  761.  
  762.         // Handle pixel data
  763.         if (bitmap.flags.bm_nobitmap) {
  764.             // No pixel data
  765.             bitmap.data = null;
  766.             console.log('Bitmap has no pixel data');
  767.             return bitmap;
  768.         }
  769.  
  770.         const baseOffset = stream.getPos();
  771.  
  772.         const { buffer: bitmapBuffer, stream: bitmapStream } = BufferUtils.createBufferSlice(
  773.             arrayBuffer,
  774.             baseOffset,
  775.             bitmap.datasize
  776.         );
  777.  
  778.         if (bitmap.flags.bm_compressed) {
  779.             if (bitmap.flags.bm_chunked) {
  780.                 // The bitmap data directly points to a ChunkHeader
  781.                 const mainHeader = new ChunkHeader(bitmapStream);
  782.  
  783.                 // Allocate the final bitmap data
  784.                 if (bitmap.flags.bm_8bit) {
  785.                     bitmap.data = new Uint8Array(bitmap.width * bitmap.height * 1);
  786.                 } else if (bitmap.flags.bm_15bit || bitmap.flags.bm_16bit) {
  787.                     bitmap.data = new Uint16Array(bitmap.width * bitmap.height * 1);
  788.                 } else if (bitmap.flags.bm_24bit) {
  789.                     // For 24-bit, we need to handle RGBTRIPLE structure
  790.                     bitmap.data = new Uint8Array(bitmap.width * bitmap.height * 3);
  791.                 } else if (bitmap.flags.bm_32bit) {
  792.                     bitmap.data = new Uint32Array(bitmap.width * bitmap.height * 1);
  793.                 }
  794.  
  795.                 // Process each block
  796.                 for (let y = 0; y < mainHeader.height; y++) {
  797.                     for (let x = 0; x < mainHeader.width; x++) {
  798.                         if (mainHeader.isBlockBlank(x, y)) {
  799.                             // This is a blank block, skip it
  800.                             continue;
  801.                         }
  802.  
  803.                         // Calculate the block width and height for each block
  804.                         const blockWidth = BitmapData.calculateBlockWidth(x, bitmap, mainHeader);
  805.                         const blockHeight = BitmapData.calculateBlockHeight(y, bitmap, mainHeader);
  806.  
  807.                         const destX = x * BitmapData.calculateBlockWidth(0, bitmap, mainHeader);
  808.                         const destY = y * BitmapData.calculateBlockHeight(0, bitmap, mainHeader);
  809.  
  810.                         // Process non-blank block
  811.                         const blockOffset = mainHeader.getBlockOffset(x, y);
  812.                         let blockSize = mainHeader.getBlockSize(x, y);
  813.                         if (blockSize === 0) {
  814.                             // This is a special case where the block size is 0,
  815.                             // which means it's the last block in the file
  816.                             blockSize = bitmapBuffer.byteLength - blockOffset;
  817.                         }
  818.                         const { buffer: blockBuffer, stream: blockStream } = BufferUtils.createBufferSlice(
  819.                             bitmapBuffer,
  820.                             blockOffset,
  821.                             blockSize
  822.                         );
  823.                         const { number, data: decompressed } = ChunkDecompressor.decompressChunk(blockStream, blockWidth, blockHeight);
  824.  
  825.                         // Copy the decompressed chunk to the right position
  826.                         if (bitmap.flags.bm_8bit) {
  827.                             // Copy each row of the decompressed data to the correct position
  828.                             for (let row = 0; row < blockHeight; row++) {
  829.                                 // Skip if we're past the bitmap height
  830.                                 if (destY + row >= bitmap.height) break;
  831.  
  832.                                 // Calculate source and destination positions for this row
  833.                                 const srcOffset = row * blockWidth;
  834.                                 const dstOffset = (destY + row) * bitmap.width + destX;
  835.  
  836.                                 // Calculate how many pixels to copy (handle edge cases)
  837.                                 const pixelsToCopy = Math.min(
  838.                                     blockWidth,                       // Pixels in this row in the block
  839.                                     bitmap.width - destX,             // Available width in destination
  840.                                     decompressed.length - srcOffset   // Available data in source
  841.                                 );
  842.  
  843.                                 // Copy the row
  844.                                 bitmap.data.set(
  845.                                     decompressed.subarray(srcOffset, srcOffset + pixelsToCopy),
  846.                                     dstOffset
  847.                                 );
  848.                             }
  849.                         } else {
  850.                             console.warn('Unsupported bitmap format, will be implmented later' + bitmap.flags);
  851.                             debugger;
  852.                         }
  853.                     }
  854.                 }
  855.             } else {
  856.                 // The bitmap data is compressed, but not chunked
  857.                 console.warn('Compressed, but not chunked bitmap data is not supported yet');
  858.                 debugger;
  859.             }
  860.         } else {
  861.             // Create a view into the pixel data based on the bit depth
  862.             // This more closely matches the union structure in the C++ code
  863.             if (bitmap.flags.bm_8bit) {
  864.                 bitmap.data = new Uint8Array(arrayBuffer, baseOffset, bitmap.datasize);
  865.             } else if (bitmap.flags.bm_15bit || bitmap.flags.bm_16bit) {
  866.                 bitmap.data = new Uint16Array(arrayBuffer, baseOffset, bitmap.datasize / 2);
  867.             } else if (bitmap.flags.bm_24bit) {
  868.                 // For 24-bit, we need to handle RGBTRIPLE structure
  869.                 bitmap.data = new Uint8Array(arrayBuffer, baseOffset, bitmap.datasize);
  870.             } else if (bitmap.flags.bm_32bit) {
  871.                 bitmap.data = new Uint32Array(arrayBuffer, baseOffset, bitmap.datasize / 4);
  872.             }
  873.         }
  874.  
  875.         // Handle palette data if present
  876.         if (bitmap.palettesize > 0 && bitmap.palette > 0) {
  877.             const paletteOffset = bitmap.palette;
  878.             const expectedSize = (256 * 2) + (256 * 4); // 256 * (2 bytes for colors + 4 bytes for rgbcolors)
  879.  
  880.             // Validate palette size
  881.             if (bitmap.palettesize !== expectedSize) {
  882.                 console.warn(`Unexpected palette size: ${bitmap.palettesize} bytes (expected ${expectedSize} bytes)`);
  883.                 debugger;
  884.             }
  885.  
  886.             // Validate that the palette data fits within the buffer
  887.             if (paletteOffset + expectedSize <= arrayBuffer.byteLength) {
  888.                 const tempBuffer = new ArrayBuffer(expectedSize);
  889.                 const tempColors = new Uint16Array(tempBuffer, 0, 256);
  890.                 const tempRGBColors = new Uint32Array(tempBuffer, 512, 256);
  891.  
  892.                 // Copy data byte by byte
  893.                 const view = new DataView(arrayBuffer);
  894.                 for (let i = 0; i < 256; i++) {
  895.                     tempColors[i] = view.getUint16(paletteOffset + i * 2, true);
  896.                     tempRGBColors[i] = view.getUint32(paletteOffset + 512 + i * 4, true);
  897.                 }
  898.  
  899.                 bitmap.palette = {
  900.                     colors: tempColors,
  901.                     rgbcolors: tempRGBColors
  902.                 };
  903.             } else {
  904.                 console.warn('Palette data extends beyond buffer bounds');
  905.             }
  906.         }
  907.  
  908.         // Handle additional buffers if present and not compressed
  909.         if (!bitmap.flags.bm_compressed) {
  910.             if (bitmap.flags.bm_zbuffer && bitmap.zbuffersize > 0) {
  911.                 // bitmap.zbuffer = new Uint16Array(
  912.                 //     arrayBuffer,
  913.                 //     baseOffset + bitmap.zbuffer,
  914.                 //     bitmap.zbuffersize / 2
  915.                 // );
  916.             }
  917.  
  918.             if (bitmap.flags.bm_normals && bitmap.normalsize > 0) {
  919.                 // bitmap.normal = new Uint16Array(
  920.                 //     arrayBuffer,
  921.                 //     baseOffset + bitmap.normal,
  922.                 //     bitmap.normalsize / 2
  923.                 // );
  924.             }
  925.  
  926.             if (bitmap.flags.bm_alpha && bitmap.alphasize > 0) {
  927.                 // bitmap.alpha = new Uint8Array(
  928.                 //     arrayBuffer,
  929.                 //     baseOffset + bitmap.alpha,
  930.                 //     bitmap.alphasize
  931.                 // );
  932.             }
  933.  
  934.             if (bitmap.flags.bm_alias && bitmap.aliassize > 0) {
  935.                 // bitmap.alias = new Uint8Array(
  936.                 //     arrayBuffer,
  937.                 //     baseOffset + bitmap.alias,
  938.                 //     bitmap.aliassize
  939.                 // );
  940.             }
  941.         }
  942.  
  943.         return bitmap;
  944.     }
  945.     // Helper functions to calculate block dimensions
  946.     static calculateBlockWidth(x, bitmap, mainHeader) {
  947.         const baseBlockWidth = Math.floor(bitmap.width / mainHeader.width);
  948.         if (x < mainHeader.width - 1) {
  949.             return baseBlockWidth;
  950.         } else {
  951.             // Last block may be wider if bitmap.width is not perfectly divisible
  952.             return bitmap.width - baseBlockWidth * (mainHeader.width - 1);
  953.         }
  954.     }
  955.  
  956.     static calculateBlockHeight(y, bitmap, mainHeader) {
  957.         const baseBlockHeight = Math.floor(bitmap.height / mainHeader.height);
  958.         if (y < mainHeader.height - 1) {
  959.             return baseBlockHeight;
  960.         } else {
  961.             // Last block may be taller if bitmap.height is not perfectly divisible
  962.             return bitmap.height - baseBlockHeight * (mainHeader.height - 1);
  963.         }
  964.     }
  965. }
  966.  
  967. class BitmapRender {
  968.     static async saveToBMP(bitmap, outputPath) {
  969.         // First, let's validate the input
  970.         if (!bitmap || !bitmap.width || !bitmap.height || !bitmap.data) {
  971.             console.error('Invalid bitmap data');
  972.             debugger;
  973.             return;
  974.         }
  975.  
  976.         const headerSize = 14;
  977.         const infoSize = 40;
  978.         const bitsPerPixel = 24;
  979.         const bytesPerPixel = bitsPerPixel / 8;
  980.  
  981.         const rowSize = Math.floor((bitsPerPixel * bitmap.width + 31) / 32) * 4;
  982.         const paddingSize = rowSize - (bitmap.width * bytesPerPixel);
  983.         const imageSize = rowSize * bitmap.height;
  984.         const fileSize = headerSize + infoSize + imageSize;
  985.  
  986.         const buffer = Buffer.alloc(fileSize);
  987.  
  988.         // Write headers...
  989.         buffer.write('BM', 0);
  990.         buffer.writeUInt32LE(fileSize, 2);
  991.         buffer.writeUInt32LE(0, 6);
  992.         buffer.writeUInt32LE(headerSize + infoSize, 10);
  993.  
  994.         buffer.writeUInt32LE(infoSize, 14);
  995.         buffer.writeInt32LE(bitmap.width, 18);
  996.         buffer.writeInt32LE(bitmap.height, 22);
  997.         buffer.writeUInt16LE(1, 26);
  998.         buffer.writeUInt16LE(bitsPerPixel, 28);
  999.         buffer.writeUInt32LE(0, 30);
  1000.         buffer.writeUInt32LE(imageSize, 34);
  1001.         buffer.writeInt32LE(0, 38);
  1002.         buffer.writeInt32LE(0, 42);
  1003.         buffer.writeUInt32LE(0, 46);
  1004.         buffer.writeUInt32LE(0, 50);
  1005.  
  1006.         let offset = headerSize + infoSize;
  1007.  
  1008.         // Pixel data writing with more defensive checks
  1009.         for (let y = bitmap.height - 1; y >= 0; y--) {
  1010.             for (let x = 0; x < bitmap.width; x++) {
  1011.                 let r = 0, g = 0, b = 0;
  1012.  
  1013.                 try {
  1014.                     if (bitmap.flags.bm_8bit && bitmap.palette) {
  1015.                         const index = y * bitmap.width + x;
  1016.                         if (index < bitmap.data.length) {
  1017.                             const paletteIndex = bitmap.data[index];
  1018.                             if (paletteIndex < 256) {
  1019.                                 if (bitmap.flags.bm_5bitpal) {
  1020.                                     // Use the 16-bit color value from colors array for 5-bit palette
  1021.                                     const colorData = bitmap.palette.colors[paletteIndex];
  1022.  
  1023.                                     // Convert 16-bit color to RGB components
  1024.                                     const red = (colorData & 0xF800) >> 11;    // Extract top 5 bits
  1025.                                     const green = (colorData & 0x07E0) >> 5;   // Extract middle 6 bits
  1026.                                     const blue = (colorData & 0x001F);         // Extract bottom 5 bits
  1027.  
  1028.                                     // Convert to 8-bit color values
  1029.                                     r = (red * 255) / 31;      // Scale 5-bit to 8-bit
  1030.                                     g = (green * 255) / 63;    // Scale 6-bit to 8-bit
  1031.                                     b = (blue * 255) / 31;     // Scale 5-bit to 8-bit
  1032.                                 } else {
  1033.                                     // Use the 32-bit rgbcolors array for regular palette
  1034.                                     const rgbColor = bitmap.palette.rgbcolors[paletteIndex];
  1035.                                     r = (rgbColor >> 16) & 0xFF;
  1036.                                     g = (rgbColor >> 8) & 0xFF;
  1037.                                     b = rgbColor & 0xFF;
  1038.                                 }
  1039.                             }
  1040.                         }
  1041.                     }
  1042.                     else if (bitmap.flags.bm_15bit) {
  1043.                         const index = y * bitmap.width + x;
  1044.                         if (index < bitmap.data.length) {
  1045.                             const pixel = bitmap.data[index];
  1046.                             r = ((pixel & 0x7C00) >> 10) << 3;
  1047.                             g = ((pixel & 0x03E0) >> 5) << 3;
  1048.                             b = (pixel & 0x001F) << 3;
  1049.                         }
  1050.                     }
  1051.                     else if (bitmap.flags.bm_16bit) {
  1052.                         const index = y * bitmap.width + x;
  1053.                         if (index < bitmap.data.length) {
  1054.                             const pixel = bitmap.data[index];
  1055.                             r = ((pixel & 0xF800) >> 11) << 3;
  1056.                             g = ((pixel & 0x07E0) >> 5) << 2;
  1057.                             b = (pixel & 0x001F) << 3;
  1058.                         }
  1059.                     }
  1060.                     else if (bitmap.flags.bm_24bit) {
  1061.                         const pixelOffset = (y * bitmap.width + x) * 3;
  1062.                         if (pixelOffset + 2 < bitmap.data.length) {
  1063.                             b = bitmap.data[pixelOffset];
  1064.                             g = bitmap.data[pixelOffset + 1];
  1065.                             r = bitmap.data[pixelOffset + 2];
  1066.                         }
  1067.                     }
  1068.                     else if (bitmap.flags.bm_32bit) {
  1069.                         const index = y * bitmap.width + x;
  1070.                         if (index < bitmap.data.length) {
  1071.                             const pixel = bitmap.data[index];
  1072.                             r = (pixel >> 16) & 0xFF;
  1073.                             g = (pixel >> 8) & 0xFF;
  1074.                             b = pixel & 0xFF;
  1075.                         }
  1076.                     }
  1077.                 } catch (error) {
  1078.                     console.warn(`Error processing pixel at ${x},${y}:`, error);
  1079.                     // Continue with default black pixel
  1080.                 }
  1081.  
  1082.                 // Write the pixel
  1083.                 buffer[offset++] = b;
  1084.                 buffer[offset++] = g;
  1085.                 buffer[offset++] = r;
  1086.             }
  1087.  
  1088.             offset += paddingSize;
  1089.         }
  1090.  
  1091.         await fs.writeFile(outputPath, buffer);
  1092.     }
  1093. }
  1094.  
  1095. // CRC32 calculation (needed for PNG format)
  1096. function calculateCRC32(data) {
  1097.     let crc = -1;
  1098.     const crcTable = new Int32Array(256);
  1099.  
  1100.     for (let n = 0; n < 256; n++) {
  1101.         let c = n;
  1102.         for (let k = 0; k < 8; k++) {
  1103.             c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
  1104.         }
  1105.         crcTable[n] = c;
  1106.     }
  1107.  
  1108.     for (let i = 0; i < data.length; i++) {
  1109.         crc = crcTable[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
  1110.     }
  1111.  
  1112.     return crc ^ -1;
  1113. }
  1114.  
  1115. class CGSResourceParser {
  1116.     static RESMAGIC = 0x52534743; // 'CGSR' in little-endian
  1117.     static RESVERSION = 1;
  1118.  
  1119.     static async loadFile(filePath) {
  1120.         try {
  1121.             const buffer = await fs.readFile(filePath);
  1122.             const arrayBuffer = buffer.buffer.slice(
  1123.                 buffer.byteOffset,
  1124.                 buffer.byteOffset + buffer.byteLength
  1125.             );
  1126.             console.info(`Reading file: ${filePath}`);
  1127.             return await CGSResourceParser.parse(arrayBuffer, filePath);
  1128.         } catch (error) {
  1129.             console.error('Error loading CGS resource file:', error);
  1130.             debugger;
  1131.             return null;
  1132.         }
  1133.     }
  1134.  
  1135.     static async parse(arrayBuffer, filePath) {
  1136.         const stream = new InputStream(arrayBuffer);
  1137.  
  1138.         // Read FileResHdr according to the C++ structure
  1139.         const header = {
  1140.             resmagic: stream.readUint32(),    // DWORD resmagic
  1141.             topbm: stream.readUint16(),       // WORD topbm
  1142.             comptype: stream.readUint8(),     // BYTE comptype
  1143.             version: stream.readUint8(),      // BYTE version
  1144.             datasize: stream.readUint32(),    // DWORD datasize
  1145.             objsize: stream.readUint32(),     // DWORD objsize
  1146.             hdrsize: stream.readUint32(),     // DWORD hdrsize
  1147.             imageryId: ImageryType.getName(stream.readUint32()),
  1148.             numStates: stream.readUint32(),
  1149.         };
  1150.  
  1151.         // Validate magic number and version
  1152.         if (header.resmagic !== this.RESMAGIC) {
  1153.             throw new Error('Not a valid CGS resource file');
  1154.         }
  1155.  
  1156.         if (header.version < this.RESVERSION) {
  1157.             throw new Error('Resource file version too old');
  1158.         }
  1159.  
  1160.         if (header.version > this.RESVERSION) {
  1161.             throw new Error('Resource file version too new');
  1162.         }
  1163.  
  1164.         if (header.objsize !== header.datasize) {
  1165.             console.warn('objsize does not match datasize');
  1166.             debugger;
  1167.         }
  1168.  
  1169.         // Read imagery_header_meta for each state
  1170.         const imageryMetaData = [];
  1171.         for (let i = 0; i < header.numStates; i++) {
  1172.             const metaData = {
  1173.                 ascii: new Uint8Array(32)  // Initialize ASCII array
  1174.             };
  1175.  
  1176.             // Read ASCII characters first
  1177.             for (let j = 0; j < 32; j++) {
  1178.                 metaData.ascii[j] = stream.readUint8();
  1179.             }
  1180.             // Convert ASCII array to string
  1181.             metaData.ascii = String.fromCharCode(...metaData.ascii)
  1182.                 .replace(/\0/g, '');  // Remove null characters
  1183.  
  1184.             // Then read the rest of the fields
  1185.             metaData.walkmap = stream.readUint32(); // Walkmap
  1186.             metaData.imageryflags = new ObjectFlags(stream.readUint32()); // Imagery state flags
  1187.             metaData.aniflags = new AnimationFlags(stream.readUint16()); // Animation state flags
  1188.             metaData.frames = stream.readUint16(); // Number of frames
  1189.             metaData.widthmax = stream.readInt16();  // Graphics maximum width/height (for IsOnScreen and refresh rects)
  1190.             metaData.heightmax = stream.readUint16();
  1191.             metaData.regx = stream.readInt16();  // Registration point x,y,z for graphics
  1192.             metaData.regy = stream.readUint16();
  1193.             metaData.regz = stream.readUint16();
  1194.             metaData.animregx = stream.readUint16(); // Registration point of animation
  1195.             metaData.animregy = stream.readUint16();
  1196.             metaData.animregz = stream.readUint16();
  1197.             metaData.wregx = stream.readUint16(); // World registration x and y of walk and bounding box info
  1198.             metaData.wregy = stream.readUint16();
  1199.             metaData.wregz = stream.readUint16();
  1200.             metaData.wwidth = stream.readUint16(); // Object's world width, length, and height for walk map and bound box
  1201.             metaData.wlength = stream.readUint16();
  1202.             metaData.wheight = stream.readUint16();
  1203.             metaData.invaniflags = new AnimationFlags(stream.readUint16()); // Animation flags for inventory animation
  1204.             metaData.invframes = stream.readUint16(); // Number of frames of inventory animation
  1205.  
  1206.             imageryMetaData.push(metaData);
  1207.         }
  1208.  
  1209.         // After all states are read, process walkmap data for states that have it
  1210.         for (const metaData of imageryMetaData) {
  1211.             if (metaData.walkmap !== 0 && metaData.wwidth > 0 && metaData.wlength > 0) {
  1212.                 const walkmapSize = metaData.wwidth * metaData.wlength;
  1213.                 metaData.walkmapData = new Uint8Array(walkmapSize);
  1214.                 for (let j = 0; j < walkmapSize; j++) {
  1215.                     metaData.walkmapData[j] = stream.readUint8();
  1216.                 }
  1217.             }
  1218.         }
  1219.  
  1220.         // Add padding to align to 4 bytes
  1221.         const totalWalkmapSize = imageryMetaData.reduce((sum, metaData) => sum + metaData.wwidth * metaData.wlength, 0);
  1222.         const padding = (4 - (totalWalkmapSize % 4)) % 4;
  1223.         if (padding > 0) {
  1224.             stream.skip(padding);
  1225.         }
  1226.  
  1227.         // Skip unknown data
  1228.         const unknownDataSize = header.hdrsize - 12 - (72 * header.numStates);
  1229.         if (unknownDataSize > 0) {
  1230.             // stream.skip(unknownDataSize);
  1231.             console.warn(`Attempt to skip ${unknownDataSize} bytes of unknown data prevented`);
  1232.             if (unknownDataSize > 4) {
  1233.                 // debugger;
  1234.             }
  1235.         }
  1236.  
  1237.         // Read bitmap offsets
  1238.         const bitmapOffsets = [];
  1239.         for (let i = 0; i < header.topbm; i++) {
  1240.             bitmapOffsets.push(stream.readUint32());
  1241.         }
  1242.  
  1243.         // Process bitmaps if present
  1244.         let bitmaps = [];
  1245.         if (bitmapOffsets && bitmapOffsets.length) {
  1246.             for (let i = 0; i < header.topbm; i++) {
  1247.                 const currentOffset = bitmapOffsets[i];
  1248.                 // Calculate size: either difference to next offset, or remaining data
  1249.                 const nextOffset = (i < header.topbm - 1)
  1250.                     ? bitmapOffsets[i + 1]
  1251.                     : header.datasize;
  1252.                 const bitmapSize = nextOffset - currentOffset;
  1253.  
  1254.                 // Create a new ArrayBuffer specifically for this bitmap
  1255.                 const bitmapBuffer = new ArrayBuffer(bitmapSize);
  1256.                 const bitmapData = new Uint8Array(bitmapBuffer);
  1257.  
  1258.                 // Copy the bitmap data from the original buffer
  1259.                 const sourceData = new Uint8Array(
  1260.                     arrayBuffer,
  1261.                     stream.getPos() + currentOffset,
  1262.                     bitmapSize
  1263.                 );
  1264.                 bitmapData.set(sourceData);
  1265.  
  1266.                 // Create a stream with the isolated bitmap data
  1267.                 const bitmapStream = new InputStream(bitmapBuffer);
  1268.  
  1269.                 // Read bitmap (now starting from position 0 since we have isolated data)
  1270.                 const bitmap = BitmapData.readBitmap(bitmapStream, bitmapBuffer);
  1271.  
  1272.                 // Get the relative path and save bitmap
  1273.                 const relativePath = filePath
  1274.                     .split('Resources/')[1]
  1275.                     .replace('.i2d', '');
  1276.  
  1277.                 const outputPath = path.join(
  1278.                     '_OUTPUT',
  1279.                     relativePath,
  1280.                     `bitmap_${i}.bmp`
  1281.                 );
  1282.  
  1283.                 await fs.mkdir(path.dirname(outputPath), { recursive: true });
  1284.                 await BitmapRender.saveToBMP(bitmap, outputPath);
  1285.  
  1286.                 // Perform sanity checks and conversions
  1287.                 if (bitmap.width > 8192 || bitmap.height > 8192) {
  1288.                     throw new Error('Corrupted bitmap list in resource');
  1289.                 }
  1290.  
  1291.                 if (bitmap.flags.bm_15bit) {
  1292.                     this.convert15to16(bitmap);
  1293.                 }
  1294.  
  1295.                 if (bitmap.flags.bm_8bit) {
  1296.                     this.convertPal15to16(bitmap);
  1297.                 }
  1298.  
  1299.                 bitmaps.push(bitmap);
  1300.             }
  1301.         }
  1302.  
  1303.         return {
  1304.             header,
  1305.             imageryMetaData,
  1306.             size: header.objsize,
  1307.             bitmaps
  1308.         };
  1309.     }
  1310.  
  1311.     static convert15to16(bitmap) {
  1312.         if (!bitmap || !bitmap.data) {
  1313.             console.warn('No bitmap data, nothing to convert.')
  1314.             return;
  1315.         }
  1316.         // Convert 15-bit color to 16-bit color
  1317.         const data = new Uint16Array(bitmap.data.buffer);
  1318.         for (let i = 0; i < data.length; i++) {
  1319.             const color15 = data[i];
  1320.             const r = (color15 & 0x7C00) >> 10;
  1321.             const g = (color15 & 0x03E0) >> 5;
  1322.             const b = color15 & 0x001F;
  1323.             data[i] = (r << 11) | (g << 6) | b;
  1324.         }
  1325.         bitmap.flags &= ~0x0002; // Clear BM_15BIT flag
  1326.     }
  1327.  
  1328.     static convertPal15to16(bitmap) {
  1329.         // Convert palette entries from 15-bit to 16-bit color
  1330.         for (let i = 0; i < bitmap.palette.length; i++) {
  1331.             const color15 = bitmap.palette[i];
  1332.             const r = (color15.r & 0x7C00) >> 10;
  1333.             const g = (color15.g & 0x03E0) >> 5;
  1334.             const b = color15.b & 0x001F;
  1335.             bitmap.palette[i] = {
  1336.                 r: (r << 11),
  1337.                 g: (g << 6),
  1338.                 b: b,
  1339.                 a: color15.a
  1340.             };
  1341.         }
  1342.     }
  1343. }
  1344.  
  1345. class ClassDefParser {
  1346.     static async loadFile(filePath) {
  1347.         try {
  1348.             const content = await fs.readFile(filePath, 'utf8');
  1349.             return ClassDefParser.parse(content);
  1350.         } catch (error) {
  1351.             console.error('Error loading class definition file:', error);
  1352.             debugger;
  1353.             return null;
  1354.         }
  1355.     }
  1356.  
  1357.     static parse(content) {
  1358.         const result = {
  1359.             uniqueTypeId: null,
  1360.             classes: new Map()
  1361.         };
  1362.  
  1363.         let currentClass = null;
  1364.         let currentSection = null;
  1365.         let inStats = false;
  1366.         let inObjStats = false;
  1367.  
  1368.         const lines = content.split('\n');
  1369.  
  1370.         for (let line of lines) {
  1371.             line = line.trim();
  1372.  
  1373.             if (line === '' || line.startsWith('//')) continue;
  1374.  
  1375.             if (line.startsWith('Unique Type ID')) {
  1376.                 result.uniqueTypeId = parseInt(line.split('=')[1].trim(), 16);
  1377.                 continue;
  1378.             }
  1379.  
  1380.             if (line.startsWith('CLASS')) {
  1381.                 currentClass = {
  1382.                     className: line.split('"')[1],
  1383.                     stats: [],        // Changed to array to maintain order
  1384.                     objStats: [],     // Changed to array to maintain order
  1385.                     types: []
  1386.                 };
  1387.                 result.classes.set(currentClass.className, currentClass);
  1388.                 currentSection = null;
  1389.                 inStats = false;
  1390.                 inObjStats = false;
  1391.                 continue;
  1392.             }
  1393.  
  1394.             if (!currentClass) continue;
  1395.  
  1396.             if (line === 'STATS') {
  1397.                 currentSection = 'stats';
  1398.                 inStats = false;
  1399.                 continue;
  1400.             } else if (line === 'OBJSTATS') {
  1401.                 currentSection = 'objStats';
  1402.                 inObjStats = false;
  1403.                 continue;
  1404.             } else if (line === 'TYPES') {
  1405.                 currentSection = 'types';
  1406.                 continue;
  1407.             }
  1408.  
  1409.             if (line === 'BEGIN') {
  1410.                 if (currentSection === 'stats') inStats = true;
  1411.                 if (currentSection === 'objStats') inObjStats = true;
  1412.                 continue;
  1413.             }
  1414.             if (line === 'END') {
  1415.                 inStats = false;
  1416.                 inObjStats = false;
  1417.                 continue;
  1418.             }
  1419.  
  1420.             if (inStats && currentSection === 'stats') {
  1421.                 const parts = line.split(' ').filter(part => part !== '');
  1422.                 if (parts.length >= 5) {
  1423.                     currentClass.stats.push({
  1424.                         name: parts[0],
  1425.                         id: parts[1],
  1426.                         default: parseInt(parts[2]),
  1427.                         min: parseInt(parts[3]),
  1428.                         max: parseInt(parts[4])
  1429.                     });
  1430.                 }
  1431.             } else if (inObjStats && currentSection === 'objStats') {
  1432.                 const parts = line.split(' ').filter(part => part !== '');
  1433.                 if (parts.length >= 5) {
  1434.                     currentClass.objStats.push({
  1435.                         name: parts[0],
  1436.                         id: parts[1],
  1437.                         default: parseInt(parts[2]),
  1438.                         min: parseInt(parts[3]),
  1439.                         max: parseInt(parts[4])
  1440.                     });
  1441.                 }
  1442.             } else if (currentSection === 'types') {
  1443.                 // Modified regex to make the stats values optional
  1444.                 const match = line.match(/"([^"]+)"\s+"([^"]+)"\s+(0x[0-9a-fA-F]+)(?:\s+{([^}]*)})?(?:\s+{([^}]*)})?/);
  1445.                 if (match) {
  1446.                     const values = match[4] ? match[4].split(',').map(v => parseInt(v.trim())) : [];
  1447.                     const extra = match[5] ? match[5].split(',').map(v => v.trim()) : [];
  1448.  
  1449.                     // Create mapped stats object only if stats exist
  1450.                     const mappedStats = {};
  1451.                     if (currentClass.stats.length > 0) {
  1452.                         currentClass.stats.forEach((stat, index) => {
  1453.                             if (index < values.length) {
  1454.                                 mappedStats[stat.name] = {
  1455.                                     value: values[index],
  1456.                                     ...stat
  1457.                                 };
  1458.                             }
  1459.                         });
  1460.                     }
  1461.  
  1462.                     // Create mapped objStats object only if objStats exist
  1463.                     const mappedObjStats = {};
  1464.                     if (currentClass.objStats.length > 0 && extra.length > 0) {
  1465.                         currentClass.objStats.forEach((stat, index) => {
  1466.                             if (index < extra.length) {
  1467.                                 mappedObjStats[stat.name] = {
  1468.                                     value: parseInt(extra[index]) || extra[index],
  1469.                                     ...stat
  1470.                                 };
  1471.                             }
  1472.                         });
  1473.                     }
  1474.  
  1475.                     currentClass.types.push({
  1476.                         name: match[1],
  1477.                         model: match[2],
  1478.                         id: parseInt(match[3], 16),
  1479.                         ...(Object.keys(mappedStats).length > 0 && { stats: mappedStats }),
  1480.                         ...(Object.keys(mappedObjStats).length > 0 && { objStats: mappedObjStats })
  1481.                     });
  1482.                 }
  1483.             }
  1484.         }
  1485.  
  1486.         return result;
  1487.     }
  1488. }
  1489.  
  1490. class DatParser {
  1491.     static SectorMapFCC = ('M'.charCodeAt(0) << 0) |
  1492.         ('A'.charCodeAt(0) << 8) |
  1493.         ('P'.charCodeAt(0) << 16) |
  1494.         (' '.charCodeAt(0) << 24);
  1495.     static MAXOBJECTCLASSES = 64;
  1496.     static OBJ_CLASSES = {
  1497.         0: 'item',
  1498.         1: 'weapon',
  1499.         2: 'armor',
  1500.         3: 'talisman',
  1501.         4: 'food',
  1502.         5: 'container',
  1503.         6: 'lightsource',
  1504.         7: 'tool',
  1505.         8: 'money',
  1506.         9: 'tile',
  1507.         10: 'exit',
  1508.         11: 'player',
  1509.         12: 'character',
  1510.         13: 'trap',
  1511.         14: 'shadow',
  1512.         15: 'helper',
  1513.         16: 'key',
  1514.         17: 'invcontainer',
  1515.         18: 'poison',
  1516.         19: 'unused1',
  1517.         20: 'unused2',
  1518.         21: 'ammo',
  1519.         22: 'scroll',
  1520.         23: 'rangedweapon',
  1521.         24: 'unused3',
  1522.         25: 'effect',
  1523.         26: 'mapscroll'
  1524.     };
  1525.     static OBJCLASS_TILE = 9;
  1526.  
  1527.     // Add static property for game directory
  1528.     static gameDir = '';
  1529.  
  1530.     // Modify the main loading function to accept gameDir
  1531.     static async loadFile(filePath, gameDir) {
  1532.         this.gameDir = gameDir; // Store gameDir for resource loading
  1533.         try {
  1534.             const buffer = await fs.readFile(filePath);
  1535.             const arrayBuffer = buffer.buffer.slice(
  1536.                 buffer.byteOffset,
  1537.                 buffer.byteOffset + buffer.byteLength
  1538.             );
  1539.  
  1540.             return DatParser.parse(arrayBuffer);
  1541.         } catch (error) {
  1542.             console.error('Error loading file:', error);
  1543.             debugger;
  1544.             return null;
  1545.         }
  1546.     }
  1547.  
  1548.     static parse(buffer) {
  1549.         const stream = new InputStream(buffer);
  1550.         let version = 0;
  1551.  
  1552.         // Read number of objects
  1553.         let numObjects = stream.readInt32();
  1554.  
  1555.         // Check if this is a sector map with header information
  1556.         if (numObjects === this.SectorMapFCC) {
  1557.             // Get sector map version
  1558.             version = stream.readInt32();
  1559.             numObjects = stream.readInt32();
  1560.         }
  1561.  
  1562.         // Array to store all loaded objects
  1563.         const objects = [];
  1564.  
  1565.         // Load each object
  1566.         for (let i = 0; i < numObjects; i++) {
  1567.             console.log(`Loading object ${i + 1} of ${numObjects}`);
  1568.             const obj = this.loadObject(stream, version, true);
  1569.             if (obj) {
  1570.                 objects.push(obj);
  1571.             }
  1572.         }
  1573.  
  1574.         return {
  1575.             version,
  1576.             numObjects,
  1577.             objects
  1578.         };
  1579.     }
  1580.  
  1581.     static classDefs = new Map();
  1582.  
  1583.     static async loadClassDefinitions(gameDir) {
  1584.         const classDefPath = path.join(gameDir, 'Resources', 'class.def');
  1585.         try {
  1586.             const classDefs = await ClassDefParser.loadFile(classDefPath);
  1587.             if (classDefs) {
  1588.                 this.classDefs = classDefs;
  1589.             }
  1590.         } catch (error) {
  1591.             console.error('Error loading class definitions:', error);
  1592.             debugger;
  1593.         }
  1594.     }
  1595.  
  1596.     static loadObject(stream, version, isMap = false) {
  1597.         let uniqueId;
  1598.         let objVersion = 0;
  1599.         let objClass;
  1600.         let objType;
  1601.         let blockSize;
  1602.         let def = {};
  1603.         let forcesimple = false;
  1604.         let corrupted = false;
  1605.  
  1606.         // ****** Load object block header ******
  1607.  
  1608.         // Get object version
  1609.         if (version >= 8) {
  1610.             objVersion = stream.readInt16();
  1611.         }
  1612.  
  1613.         if (objVersion < 0) { // Objversion is the placeholder in map version 8 or above
  1614.             return null;
  1615.         }
  1616.  
  1617.         objClass = stream.readInt16();
  1618.         if (objClass < 0) {   // Placeholder for empty object slot
  1619.             return null;
  1620.         }
  1621.  
  1622.         // Check the sector map version before we read the type info
  1623.         if (version < 1) {
  1624.             // Version 0 - No Unique ID's, so just read the objtype directly
  1625.             objType = stream.readInt16();
  1626.             uniqueId = 0;
  1627.             blockSize = -1;
  1628.         }
  1629.         else if (version < 4) {
  1630.             // Version 1 and above - Unique ID's used instead of objtype
  1631.             objType = -1;
  1632.             uniqueId = stream.readUint32();
  1633.             blockSize = -1;
  1634.         }
  1635.         else {
  1636.             // Version 4 has block size
  1637.             objType = -1;
  1638.             uniqueId = stream.readUint32();
  1639.             blockSize = stream.readInt16();
  1640.         }
  1641.  
  1642.         // ****** Is this object any good? ******
  1643.  
  1644.         const cl = this.getObjectClass(objClass);
  1645.         if (!cl) {
  1646.             if (this.Debug) {
  1647.                 throw new Error("Object in map file has invalid class - possible file corruption");
  1648.             }
  1649.             else if (blockSize >= 0) {
  1650.                 stream.skip(blockSize);  // Just quietly skip this object
  1651.                 return null;
  1652.             }
  1653.             else {                      // Try to fix it by assuming its a tile
  1654.                 objClass = this.OBJCLASS_TILE;
  1655.                 corrupted = true;
  1656.             }
  1657.         }
  1658.  
  1659.         if (objType < 0) {
  1660.             objType = this.findObjectType(uniqueId, objClass);
  1661.  
  1662.             if (objType < 0) {
  1663.                 // not found in this class, so check all of them
  1664.                 for (let newObjClass = 0; newObjClass < this.MAXOBJECTCLASSES; newObjClass++) {
  1665.                     const newType = this.findObjectType(uniqueId, newObjClass);
  1666.                     if (newType >= 0) {
  1667.                         objClass = newObjClass;
  1668.                         objType = newType;
  1669.                         forcesimple = true;
  1670.                         break;
  1671.                     }
  1672.                 }
  1673.             }
  1674.  
  1675.             if (objType < 0) {  // Still can't find type
  1676.                 if (this.Debug) {
  1677.                     throw new Error(`Object unique id 0x${uniqueId.toString(16)} not found in class.def`);
  1678.                 }
  1679.                 else if (blockSize >= 0) {    // Just skip over this object
  1680.                     stream.skip(blockSize);
  1681.                     return null;
  1682.                 }
  1683.                 else {      // If attempting to fix, assume type is type 0
  1684.                     objType = 0;
  1685.                     corrupted = true;
  1686.                 }
  1687.             }
  1688.         }
  1689.  
  1690.         // ****** Create the object ******
  1691.  
  1692.         def.objClass = objClass;
  1693.         def.objType = objType;
  1694.  
  1695.         // Get start of object
  1696.         const startPos = stream.getPos();
  1697.         const typeInfo = this.getTypeInfo(uniqueId, objClass)
  1698.         // Load object data
  1699.         const objectData = forcesimple ?
  1700.             this.loadBaseObjectData(stream, version, objVersion) :  // Used if object changed class
  1701.             this.loadObjectData(stream, version, objVersion, typeInfo, this.OBJ_CLASSES[objClass]);       // This should normally be used
  1702.  
  1703.         const inventory = this.loadInventory(stream, version);
  1704.  
  1705.         // Reset position to start of next object
  1706.         if (blockSize >= 0) {
  1707.             stream.setPos(startPos + blockSize);
  1708.         }
  1709.  
  1710.         // If this object is corrupted in some way, return null
  1711.         if (corrupted || (isMap && this.hasNonMapFlag(objectData))) {
  1712.             return null;
  1713.         }
  1714.  
  1715.         return {
  1716.             version: objVersion,
  1717.             class: {
  1718.                 id: objClass,
  1719.                 name: this.OBJ_CLASSES[objClass] || 'unknown'
  1720.             },
  1721.             type: objType,
  1722.             typeInfo,
  1723.             uniqueId,
  1724.             blockSize,
  1725.             data: objectData,
  1726.             inventory
  1727.         };
  1728.     }
  1729.  
  1730.     static loadBaseObjectData(stream, version, objVersion) {
  1731.         // Read name (length-prefixed string)
  1732.         const name = stream.readString();
  1733.  
  1734.         // Note: we might need to adjust the flags handling since we're using ObjectFlags class
  1735.         // For now, let's store both raw value and parsed flags
  1736.         const flagsRaw = stream.readUint32();
  1737.         const flags = new ObjectFlags(flagsRaw);
  1738.  
  1739.         const position = {
  1740.             x: stream.readInt32(),
  1741.             y: stream.readInt32(),
  1742.             z: stream.readInt32()
  1743.         };
  1744.  
  1745.         // Read velocity if mobile and version < 6
  1746.         let velocity = { x: 0, y: 0, z: 0 };
  1747.         if (version < 6 || !flags.of_immobile) {
  1748.             velocity = {
  1749.                 x: stream.readInt32(),
  1750.                 y: stream.readInt32(),
  1751.                 z: stream.readInt32()
  1752.             };
  1753.         }
  1754.  
  1755.         // Read state
  1756.         let state;
  1757.         if (version < 9) {
  1758.             state = stream.readUint8();
  1759.         } else {
  1760.             state = stream.readUint16();
  1761.         }
  1762.  
  1763.         // Handle level for non-map objects
  1764.         let level = 0;
  1765.         if (version >= 6 && flags.of_nonmap) {
  1766.             if (version < 9) {
  1767.                 level = stream.readUint8();
  1768.             } else {
  1769.                 level = stream.readUint16();
  1770.             }
  1771.         }
  1772.  
  1773.         // Handle health for old versions
  1774.         let health;
  1775.         if (version < 5) {
  1776.             health = stream.readUint8();
  1777.         }
  1778.  
  1779.         // Read inventory and rotation data
  1780.         let inventNum, invIndex, shadow, rotateX, rotateY, rotateZ, mapIndex;
  1781.         if (version < 3) {
  1782.             const facing = stream.readUint8();
  1783.             const dummy16 = stream.readInt16();
  1784.             inventNum = stream.readInt16();
  1785.             const dummy16_2 = stream.readInt16();
  1786.             shadow = stream.readInt32();
  1787.             const dummy8 = stream.readUint8();
  1788.  
  1789.             // ignore inventories in old version
  1790.             inventNum = -1;
  1791.             mapIndex = -1;
  1792.         } else {
  1793.             inventNum = stream.readInt16();
  1794.             invIndex = stream.readInt16();
  1795.             shadow = stream.readInt32();
  1796.             rotateX = stream.readUint8();
  1797.             rotateY = stream.readUint8();
  1798.             rotateZ = stream.readUint8();
  1799.             mapIndex = stream.readInt32();
  1800.         }
  1801.  
  1802.         // Handle animation and stats
  1803.         let frame = 0;
  1804.         let frameRate = 1;
  1805.         let group = 0;
  1806.         let stats = [];
  1807.  
  1808.         if (version < 5) {
  1809.             // Set up empty stat array and stick health in it
  1810.             if (this.getNumObjStats() > 0) {
  1811.                 stats = new Array(this.getNumObjStats()).fill(0);
  1812.                 this.setHealth(health, stats);
  1813.             }
  1814.         } else {
  1815.             if (version >= 6) {
  1816.                 if (flags.of_animate) {
  1817.                     frame = stream.readInt16();
  1818.                     frameRate = stream.readInt16();
  1819.                 }
  1820.             } else {
  1821.                 frame = stream.readInt16();
  1822.                 frameRate = stream.readInt16();
  1823.             }
  1824.  
  1825.             group = stream.readUint8();
  1826.  
  1827.             // Read stats
  1828.             const numStats = stream.readUint8();
  1829.             if (numStats > 0) {
  1830.                 stats = [];
  1831.                 for (let st = 0; st < numStats; st++) {
  1832.                     const stat = stream.readInt32();
  1833.                     const uniqueId = stream.readUint32();
  1834.                     stats.push({ stat, uniqueId });
  1835.                 }
  1836.             }
  1837.         }
  1838.  
  1839.         // Read light data if present
  1840.         let lightDef = null;
  1841.         if (flags.of_light) {
  1842.             lightDef = new SLightDef();
  1843.  
  1844.             // Read flags
  1845.             const lightFlags = stream.readUint8();
  1846.             lightDef.flags = new LightFlags(lightFlags);
  1847.  
  1848.             // Read position
  1849.             lightDef.pos = new S3DPoint(
  1850.                 stream.readInt32(),  // x
  1851.                 stream.readInt32(),  // y
  1852.                 stream.readInt32()   // z
  1853.             );
  1854.  
  1855.             // Read color
  1856.             lightDef.color = new SColor(
  1857.                 stream.readUint8(),  // red
  1858.                 stream.readUint8(),  // green
  1859.                 stream.readUint8()   // blue
  1860.             );
  1861.  
  1862.             // Read intensity and multiplier
  1863.             lightDef.intensity = stream.readUint8();
  1864.             lightDef.multiplier = stream.readInt16();
  1865.  
  1866.             // Set the light and animate flags using our ObjectFlags properties
  1867.             flags.of_light = true;
  1868.             flags.of_animate = true;
  1869.         }
  1870.  
  1871.  
  1872.         return {
  1873.             name,
  1874.             flags,         // This will be the ObjectFlags instance
  1875.             flagsRaw,     // This is the raw uint32 value
  1876.             position,
  1877.             velocity,
  1878.             state,
  1879.             level,
  1880.             inventNum,
  1881.             invIndex,
  1882.             shadow,
  1883.             rotation: {
  1884.                 x: rotateX,
  1885.                 y: rotateY,
  1886.                 z: rotateZ
  1887.             },
  1888.             mapIndex,
  1889.             frame,
  1890.             frameRate,
  1891.             group,
  1892.             stats,
  1893.             lightDef
  1894.         };
  1895.     }
  1896.  
  1897.     static getNumObjStats() {
  1898.         // Implement this method to return the number of object stats
  1899.         return 0;
  1900.     }
  1901.  
  1902.     static setHealth(health, stats) {
  1903.         // Implement this method to set health in stats array
  1904.         if (stats.length > 0) {
  1905.             stats[0] = health;
  1906.         }
  1907.     }
  1908.  
  1909.     static Debug = false; // Add this class property
  1910.  
  1911.     static readBaseObjectData(stream) {
  1912.         return {
  1913.             name: stream.readString(),
  1914.             flags: new ObjectFlags(stream.readUint32()),
  1915.             position: {
  1916.                 x: stream.readInt32(),
  1917.                 y: stream.readInt32(),
  1918.                 z: stream.readInt32()
  1919.             }
  1920.         };
  1921.     }
  1922.  
  1923.     static readBaseObjectDataAfterPos(stream) {
  1924.         return {
  1925.             state: stream.readUint16(),
  1926.             inventNum: stream.readInt16(),
  1927.             inventIndex: stream.readInt16(),
  1928.             shadowMapId: stream.readInt32(),
  1929.             rotation: {
  1930.                 x: stream.readUint8(),
  1931.                 y: stream.readUint8(),
  1932.                 z: stream.readUint8()
  1933.             },
  1934.             mapIndex: stream.readInt32()
  1935.         };
  1936.     }
  1937.  
  1938.     static readVelocityData(stream) {
  1939.         return {
  1940.             velocity: {
  1941.                 x: stream.readInt32(),
  1942.                 y: stream.readInt32(),
  1943.                 z: stream.readInt32()
  1944.             }
  1945.         };
  1946.     }
  1947.  
  1948.     static readObjectStats(stream) {
  1949.         const numStats = stream.readUint8();
  1950.         const stats = [];
  1951.  
  1952.         for (let i = 0; i < numStats; i++) {
  1953.             stats.push({
  1954.                 value: stream.readInt32(),
  1955.                 encryptedId: stream.readUint32()
  1956.             });
  1957.         }
  1958.  
  1959.         return stats;
  1960.     }
  1961.  
  1962.     static readCharacterData(stream) {
  1963.         const complexObjVer = stream.readUint8();
  1964.         const charObjVer = stream.readUint8();
  1965.  
  1966.         const baseData = this.readBaseObjectData(stream);
  1967.         const velocityData = this.readVelocityData(stream);
  1968.         const baseDataAfterPos = this.readBaseObjectDataAfterPos(stream);
  1969.  
  1970.         return {
  1971.             complexObjVer,
  1972.             charObjVer,
  1973.             ...baseData,
  1974.             ...velocityData,
  1975.             ...baseDataAfterPos,
  1976.             frame: stream.readInt16(),
  1977.             frameRate: stream.readInt16(),
  1978.             group: stream.readUint8(),
  1979.             stats: this.readObjectStats(stream),
  1980.             actionCode: stream.readUint8(),
  1981.             actionName: stream.readString(),
  1982.             timestamps: {
  1983.                 lastHealth: stream.readUint32(),
  1984.                 lastFatigue: stream.readUint32(),
  1985.                 lastMana: stream.readUint32(),
  1986.                 lastPoison: stream.readUint32()
  1987.             },
  1988.             teleport: {
  1989.                 x: stream.readInt32(),
  1990.                 y: stream.readInt32(),
  1991.                 z: stream.readInt32(),
  1992.                 level: stream.readInt32()
  1993.             }
  1994.         };
  1995.     }
  1996.  
  1997.     static readObjectData(stream, objClass, dataSize) {
  1998.         const startPos = stream.getPos();
  1999.  
  2000.         let data;
  2001.         switch (objClass) {
  2002.             case 12: // character
  2003.                 data = this.readCharacterData(stream);
  2004.                 break;
  2005.             case 5: // container
  2006.                 data = {
  2007.                     ...this.readBaseObjectData(stream),
  2008.                     ...this.readVelocityData(stream),
  2009.                     ...this.readBaseObjectDataAfterPos(stream),
  2010.                     numItems: stream.readUint32()
  2011.                 };
  2012.                 break;
  2013.             default:
  2014.                 data = {
  2015.                     ...this.readBaseObjectData(stream),
  2016.                     ...this.readBaseObjectDataAfterPos(stream)
  2017.                 };
  2018.         }
  2019.  
  2020.         // Ensure we've read exactly dataSize bytes
  2021.         const bytesRead = stream.getPos() - startPos;
  2022.         if (bytesRead < dataSize) {
  2023.             stream.skip(dataSize - bytesRead);
  2024.         }
  2025.  
  2026.         return data;
  2027.     }
  2028.  
  2029.     static getObjectClass(classId) {
  2030.         // For now, just check if it's a valid class ID
  2031.         return this.OBJ_CLASSES.hasOwnProperty(classId);
  2032.     }
  2033.  
  2034.     static findObjectType(uniqueId, classId) {
  2035.         // Get class name from classId
  2036.         const className = this.OBJ_CLASSES[classId];
  2037.         if (!className) return -1;
  2038.  
  2039.         // Get class definition from our loaded class definitions
  2040.         const classDefs = this.classDefs.classes;
  2041.         const classDef = classDefs.get(className.toUpperCase());
  2042.         if (!classDef) return -1;
  2043.  
  2044.         // Find the type with matching uniqueId
  2045.         const typeIndex = classDef.types.findIndex(t => t.id === uniqueId);
  2046.         if (typeIndex !== -1) {
  2047.             return typeIndex;
  2048.         }
  2049.  
  2050.         // If not found in the expected class, optionally search all classes
  2051.         for (const [otherClassName, otherClassDef] of classDefs) {
  2052.             if (otherClassName !== className.toUpperCase()) {
  2053.                 const index = otherClassDef.types.findIndex(t => t.id === uniqueId);
  2054.                 if (index !== -1) {
  2055.                     console.warn(`Found object type ${uniqueId.toString(16)} in class ${otherClassName} instead of ${className}`);
  2056.                     return index;
  2057.                 }
  2058.             }
  2059.         }
  2060.  
  2061.         // If still not found, return -1
  2062.         console.warn(`Could not find object type ${uniqueId.toString(16)} in class ${className}`);
  2063.         return -1;
  2064.     }
  2065.  
  2066.     // Add a helper method to get type information
  2067.     static getTypeInfo(uniqueId, classId) {
  2068.         const className = this.OBJ_CLASSES[classId];
  2069.         if (!className) return null;
  2070.  
  2071.         const classDefs = this.classDefs.classes;
  2072.         const classDef = classDefs.get(className.toUpperCase());
  2073.         if (!classDef) return null;
  2074.  
  2075.         return classDef.types.find(t => t.id === uniqueId) || null;
  2076.     }
  2077.  
  2078.     static fileCache = new Map(); // Cache for file paths
  2079.  
  2080.     static async buildFileCache(baseDir) {
  2081.         const cache = new Map();
  2082.  
  2083.         async function scanDirectory(dir) {
  2084.             const entries = await fs.readdir(dir, { withFileTypes: true });
  2085.  
  2086.             for (const entry of entries) {
  2087.                 const fullPath = path.join(dir, entry.name);
  2088.                 const relativePath = path.relative(baseDir, fullPath).toLowerCase();
  2089.  
  2090.                 if (entry.isDirectory()) {
  2091.                     await scanDirectory(fullPath);
  2092.                 } else {
  2093.                     cache.set(relativePath, fullPath);
  2094.                 }
  2095.             }
  2096.         }
  2097.  
  2098.         await scanDirectory(baseDir);
  2099.         return cache;
  2100.     }
  2101.  
  2102.     static async findRealPath(baseDir, searchPath) {
  2103.         // Normalize the search path
  2104.         const normalizedSearch = searchPath.toLowerCase().replace(/\\/g, path.sep);
  2105.  
  2106.         // Initialize cache if needed
  2107.         if (this.fileCache.size === 0) {
  2108.             this.fileCache = await this.buildFileCache(baseDir);
  2109.         }
  2110.  
  2111.         // Look up the real path in the cache
  2112.         const realPath = this.fileCache.get(normalizedSearch);
  2113.         if (realPath) {
  2114.             return realPath;
  2115.         }
  2116.  
  2117.         return null;
  2118.     }
  2119.  
  2120.     static async loadResourceFile(gameDir, resourcePath) {
  2121.         try {
  2122.             const resourcesDir = path.join(gameDir, 'Resources');
  2123.  
  2124.             // Prepend 'Imagery' to the resource path
  2125.             const imageryPath = path.join('Imagery', resourcePath);
  2126.  
  2127.             const realPath = await this.findRealPath(resourcesDir, imageryPath);
  2128.  
  2129.             if (!realPath) {
  2130.                 console.warn(`Resource file not found: ${resourcePath}`);
  2131.                 return null;
  2132.             }
  2133.  
  2134.             const resource = await CGSResourceParser.loadFile(realPath);
  2135.             return resource;
  2136.         } catch (error) {
  2137.             console.error(`Error loading resource file ${resourcePath}:`, error);
  2138.             debugger;
  2139.             return null;
  2140.         }
  2141.     }
  2142.  
  2143.     static loadObjectData(stream, version, objVersion, typeInfo, objClassName) {
  2144.         switch (objClassName.toLowerCase()) {
  2145.             case 'tile':
  2146.             case 'effect':
  2147.             case 'helper':
  2148.             case 'shadow':
  2149.             case 'trap':
  2150.             case 'food':
  2151.             case 'item':
  2152.                 return this.loadBaseObjectData(stream, version, objVersion);
  2153.             case 'exit':
  2154.                 return this.loadExitData(stream, version, objVersion);
  2155.             case 'container':
  2156.                 return this.loadContainerData(stream, version, objVersion);
  2157.             case 'complexobject':
  2158.                 return this.loadComplexObjectData(stream, version, objVersion);
  2159.             case 'character':
  2160.                 return this.loadCharacterData(stream, version, objVersion);
  2161.             case 'scroll':
  2162.                 return this.loadScrollData(stream, version, objVersion);
  2163.             case 'weapon':
  2164.                 return this.loadWeaponData(stream, version, objVersion);
  2165.             default:
  2166.                 console.warn(`Unknown object class: ${objClassName}`);
  2167.                 debugger;
  2168.                 return {};
  2169.         }
  2170.     }
  2171.  
  2172.     static loadWeaponData(stream, version, objVersion) {
  2173.         // Load base object data first
  2174.         const baseData = this.loadBaseObjectData(stream, version, objVersion);
  2175.  
  2176.         // Read poison value
  2177.         const poison = stream.readInt32();
  2178.  
  2179.         return {
  2180.             ...baseData,
  2181.             className: 'weapon',
  2182.             poison,
  2183.  
  2184.             // Add helper methods and getters for stats
  2185.             getPoison: () => poison,
  2186.             getType: () => baseData.stats?.find(s => s.name === "Type")?.value ?? 0,
  2187.             getDamage: () => baseData.stats?.find(s => s.name === "Damage")?.value ?? 0,
  2188.             getEqSlot: () => baseData.stats?.find(s => s.name === "EqSlot")?.value ?? 0,
  2189.             getCombining: () => baseData.stats?.find(s => s.name === "Combining")?.value ?? 0,
  2190.             getValue: () => baseData.stats?.find(s => s.name === "Value")?.value ?? 0,
  2191.  
  2192.             // Helper method to clear weapon (matches C++ ClearWeapon())
  2193.             clearWeapon: function () {
  2194.                 this.poison = 0;
  2195.             }
  2196.         };
  2197.     }
  2198.  
  2199.     static loadScrollData(stream, version, objVersion) {
  2200.         // Load base object data first
  2201.         const baseData = this.loadBaseObjectData(stream, version, objVersion);
  2202.  
  2203.         // Read text length
  2204.         const textLength = stream.readInt16();
  2205.  
  2206.         let text = null;
  2207.         if (textLength > 0) {
  2208.             // Read text characters
  2209.             const textBytes = new Uint8Array(textLength);
  2210.             for (let i = 0; i < textLength; i++) {
  2211.                 textBytes[i] = stream.readUint8();
  2212.             }
  2213.             // Convert to string
  2214.             text = new TextDecoder('ascii').decode(textBytes);
  2215.         }
  2216.  
  2217.         return {
  2218.             ...baseData,
  2219.             className: 'scroll',
  2220.             text,
  2221.  
  2222.             // Add helper methods
  2223.             getText: () => text,
  2224.             cursorType: (inst) => inst ? CURSOR_NONE : CURSOR_EYE
  2225.         };
  2226.     }
  2227.  
  2228.     static loadCharacterData(stream, version, objVersion) {
  2229.         let baseData;
  2230.  
  2231.         // Load base complex object data based on version
  2232.         if (objVersion >= 3) {
  2233.             // Read complex object version byte first
  2234.             const complexObjVersion = stream.readUint8();
  2235.             baseData = this.loadComplexObjectData(stream, version, complexObjVersion);
  2236.         } else {
  2237.             baseData = this.loadComplexObjectData(stream, version, 0);
  2238.         }
  2239.  
  2240.         // Early return for old versions
  2241.         if (objVersion < 1) {
  2242.             return {
  2243.                 ...baseData,
  2244.                 className: 'character'
  2245.             };
  2246.         }
  2247.  
  2248.         // Load recovery timestamps
  2249.         const lasthealthrecov = stream.readInt32();
  2250.         const lastfatiguerecov = stream.readInt32();
  2251.         let lastmanarecov = -1;
  2252.         try {
  2253.  
  2254.             lastmanarecov = stream.readInt32();
  2255.         } catch (error) {
  2256.             debugger
  2257.         }
  2258.  
  2259.         // Load poison damage (version 4+)
  2260.         let lastpoisondamage = -1;
  2261.         if (objVersion >= 4) {
  2262.             lastpoisondamage = stream.readInt32();
  2263.         }
  2264.  
  2265.         // Load teleport data (version 2+)
  2266.         let teleportPosition = new S3DPoint(-1, -1, -1);
  2267.         let teleportLevel = -1;
  2268.         if (objVersion >= 2) {
  2269.             teleportPosition = new S3DPoint(
  2270.                 stream.readInt32(),  // x
  2271.                 stream.readInt32(),  // y
  2272.                 stream.readInt32()   // z
  2273.             );
  2274.             teleportLevel = stream.readInt32();
  2275.         }
  2276.  
  2277.         return {
  2278.             ...baseData,
  2279.             className: 'character',
  2280.             lasthealthrecov,
  2281.             lastfatiguerecov,
  2282.             lastmanarecov,
  2283.             lastpoisondamage,
  2284.             teleportPosition,
  2285.             teleportLevel
  2286.         };
  2287.     }
  2288.  
  2289.     static loadComplexObjectData(stream, version, objVersion) {
  2290.         let baseData;
  2291.  
  2292.         // Load base object data based on version
  2293.         if (objVersion >= 1) {
  2294.             // Read base class version byte first
  2295.             const baseObjVersion = stream.readUint8();
  2296.             baseData = this.loadBaseObjectData(stream, version, baseObjVersion);
  2297.         } else {
  2298.             baseData = this.loadBaseObjectData(stream, version, 0);
  2299.         }
  2300.  
  2301.         // Load root state
  2302.         let actionBlock;
  2303.         if (version < 7) {
  2304.             actionBlock = new ActionBlock("still"); // DefaultRootState
  2305.         } else {
  2306.             const action = stream.readUint8();
  2307.             const name = stream.readString();
  2308.  
  2309.             actionBlock = new ActionBlock(name, action);
  2310.         }
  2311.  
  2312.         return {
  2313.             ...baseData,
  2314.             className: 'complexobject',
  2315.             root: actionBlock,
  2316.             doing: actionBlock,
  2317.             desired: actionBlock,
  2318.             state: -1
  2319.         };
  2320.     }
  2321.  
  2322.     static loadContainerData(stream, version, objVersion) {
  2323.         // Load base object data first
  2324.         const baseData = this.loadBaseObjectData(stream, version, objVersion);
  2325.  
  2326.         // Handle container-specific data for versions 2-4
  2327.         if (version >= 2 && version < 5) {
  2328.             const contflags = stream.readInt32();
  2329.             const pickdifficulty = stream.readInt32();
  2330.  
  2331.             baseData.stats = baseData.stats || [];
  2332.             baseData.stats.push(
  2333.                 { name: "Locked", value: contflags !== 0 },
  2334.                 { name: "PickDifficulty", value: pickdifficulty }
  2335.             );
  2336.         }
  2337.  
  2338.         return baseData;
  2339.     }
  2340.  
  2341.     static loadExitData(stream, version, objVersion) {
  2342.         // Load container data first (which includes base object data)
  2343.         const containerData = this.loadContainerData(stream, version, objVersion);
  2344.  
  2345.         // Load TExit specific data
  2346.         const exitflags = stream.readUint32();
  2347.  
  2348.         return {
  2349.             ...containerData,
  2350.             exitflags,
  2351.             className: 'exit',
  2352.             isOn: () => !!(exitflags & ExitFlags.EX_ON),
  2353.             isActivated: () => !!(exitflags & ExitFlags.EX_ACTIVATED),
  2354.             isFromExit: () => !!(exitflags & ExitFlags.EX_FROMEXIT)
  2355.         };
  2356.     }
  2357.  
  2358.     static loadInventory(stream, version) {
  2359.         // Early return for versions < 3
  2360.         if (version < 3) {
  2361.             return [];
  2362.         }
  2363.  
  2364.         // Read number of inventory items
  2365.         const num = stream.readInt32();
  2366.  
  2367.         // Sanity check for inventory size
  2368.         if (num > 2048) {
  2369.             console.warn("Invalid inventory size:", num);
  2370.             return [];
  2371.         }
  2372.  
  2373.         // Array to store inventory items
  2374.         const inventory = [];
  2375.  
  2376.         // Load each inventory object
  2377.         for (let i = 0; i < num; i++) {
  2378.             try {
  2379.                 // the code below is commented out because it's not working properly
  2380.                 // it supposed to load objects into inventory recursevely, but it's not working
  2381.                 // when we attempt to read inventory from the dat map file for an object like a
  2382.                 // chatacter, it starts to read garbage. I coulnd't figure out why or find where
  2383.                 // the problem is.
  2384.                 // Anyways, for the purpose of building a map inventory is not needed anyways.
  2385.                 // const inst = this.loadObject(stream, version);
  2386.                 console.log("Skipping loading inventory object " + i);
  2387.                 continue;
  2388.                 if (inst) {
  2389.                     // In the C++ version, inst->SetOwner(this) is called
  2390.                     // We might need to implement something similar depending on our needs
  2391.                     inst.owner = this; // or however we handle ownership
  2392.                     inventory.push(inst);
  2393.                 } else {
  2394.                     console.warn("Invalid inventory object loaded");
  2395.                 }
  2396.             } catch (error) {
  2397.                 console.warn("Error loading inventory object:", error);
  2398.                 // Continue loading other items even if one fails
  2399.             }
  2400.         }
  2401.  
  2402.         return inventory;
  2403.     }
  2404.  
  2405.     static hasNonMapFlag(objectData) {
  2406.         return objectData.flags.of_nonmap;
  2407.     }
  2408. }
  2409.  
  2410. // Exit states
  2411. const ExitStates = {
  2412.     EXIT_CLOSED: 0,
  2413.     EXIT_OPEN: 1,
  2414.     EXIT_CLOSING: 2,
  2415.     EXIT_OPENING: 3
  2416. };
  2417.  
  2418. // Exit flags
  2419. const ExitFlags = {
  2420.     EX_ON: 1 << 0,        // player is on exit strip
  2421.     EX_ACTIVATED: 1 << 1,  // exit has been activated
  2422.     EX_FROMEXIT: 1 << 2   // player just came from another exit.. don't do anything
  2423. };
  2424.  
  2425. class ExitRef {
  2426.     constructor() {
  2427.         this.name = '';           // name of exit
  2428.         this.target = null;       // position on level (S3DPoint)
  2429.         this.level = 0;          // level to change to
  2430.         this.mapindex = 0;       // object character is transfered to (usually another exit)
  2431.         this.ambient = 0;        // level of ambient light
  2432.         this.ambcolor = null;    // color of ambient light (SColor)
  2433.         this.next = null;        // next in list
  2434.     }
  2435. }
  2436.  
  2437. // Weapon type constants
  2438. const WeaponType = {
  2439.     WT_HAND: 0,        // Hand, claw, tail, etc.  
  2440.     WT_KNIFE: 1,       // Daggers, knives
  2441.     WT_SWORD: 2,       // Swords
  2442.     WT_BLUDGEON: 3,    // Clubs, maces, hammers
  2443.     WT_AXE: 4,         // Axes
  2444.     WT_STAFF: 5,       // Staffs, polearms, spears, etc.
  2445.     WT_BOW: 6,         // Bow
  2446.     WT_CROSSBOW: 7,    // Crossbow
  2447.     WT_LAST: 7         // Last weapon type
  2448. };
  2449.  
  2450. // Weapon mask constants
  2451. const WeaponMask = {
  2452.     WM_HAND: 0x0001,
  2453.     WM_KNIFE: 0x0002,
  2454.     WM_SWORD: 0x0004,
  2455.     WM_BLUDGEON: 0x0008,
  2456.     WM_AXE: 0x0010,
  2457.     WM_STAFF: 0x0020,
  2458.     WM_BOW: 0x0040,
  2459.     WM_CROSSBOW: 0x0080
  2460. };
  2461.  
  2462. const ActionTypes = {
  2463.     ACTION_NONE: 0,
  2464.     ACTION_ANIMATE: 1,
  2465.     ACTION_MOVE: 2,
  2466.     ACTION_COMBAT: 3,
  2467.     ACTION_COMBATMOVE: 4,
  2468.     ACTION_COMBATLEAP: 5,
  2469.     ACTION_COLLAPSE: 6,
  2470.     ACTION_ATTACK: 7,
  2471.     ACTION_BLOCK: 8,
  2472.     ACTION_DODGE: 9,
  2473.     ACTION_MISS: 10,
  2474.     ACTION_INVOKE: 11,
  2475.     ACTION_IMPACT: 12,
  2476.     ACTION_STUN: 13,
  2477.     ACTION_KNOCKDOWN: 14,
  2478.     ACTION_FLYBACK: 15,
  2479.     ACTION_SAY: 16,
  2480.     ACTION_PIVOT: 17,
  2481.     ACTION_PULL: 18,
  2482.     ACTION_DEAD: 19,
  2483.     ACTION_PULP: 20,
  2484.     ACTION_BURN: 21,
  2485.     ACTION_FLAIL: 22,
  2486.     ACTION_SLEEP: 23,
  2487.     ACTION_LEAP: 24,
  2488.     ACTION_BOW: 25,
  2489.     ACTION_BOWMOVE: 26,
  2490.     ACTION_BOWAIM: 27,
  2491.     ACTION_BOWSHOOT: 28
  2492. };
  2493.  
  2494. class ActionBlock {
  2495.     constructor(name, action = ActionTypes.ACTION_ANIMATE) {
  2496.         this.action = action;
  2497.         this.name = name;
  2498.         this.frame = 0;
  2499.         this.wait = 0;
  2500.         this.angle = 0;
  2501.         this.moveangle = 0;
  2502.         this.turnrate = 0;
  2503.         this.target = null;  // S3DPoint
  2504.         this.obj = null;     // ObjectInstance reference
  2505.         this.attack = null;
  2506.         this.impact = null;
  2507.         this.damage = 0;
  2508.         this.data = null;
  2509.         this.flags = 0;
  2510.     }
  2511. }
  2512.  
  2513. // Light flags
  2514. class LightFlags {
  2515.     static LIGHT_DIR = 1 << 0;  // Directional light
  2516.     static LIGHT_SUN = 1 << 1;  // Sunlight
  2517.     static LIGHT_MOON = 1 << 2; // Moonlight
  2518.  
  2519.     constructor(value) {
  2520.         this.isDirectional = !!(value & LightFlags.LIGHT_DIR);
  2521.         this.isSunlight = !!(value & LightFlags.LIGHT_SUN);
  2522.         this.isMoonlight = !!(value & LightFlags.LIGHT_MOON);
  2523.     }
  2524. }
  2525.  
  2526. class S3DPoint {
  2527.     constructor(x = 0, y = 0, z = 0) {
  2528.         this.x = x;
  2529.         this.y = y;
  2530.         this.z = z;
  2531.     }
  2532.  
  2533.     add(other) {
  2534.         return new S3DPoint(
  2535.             this.x + other.x,
  2536.             this.y + other.y,
  2537.             this.z + other.z
  2538.         );
  2539.     }
  2540.  
  2541.     subtract(other) {
  2542.         return new S3DPoint(
  2543.             this.x - other.x,
  2544.             this.y - other.y,
  2545.             this.z - other.z
  2546.         );
  2547.     }
  2548.  
  2549.     multiply(scalar) {
  2550.         return new S3DPoint(
  2551.             this.x * scalar,
  2552.             this.y * scalar,
  2553.             this.z * scalar
  2554.         );
  2555.     }
  2556.  
  2557.     inRange(pos, dist) {
  2558.         const absX = Math.abs(this.x - pos.x);
  2559.         const absY = Math.abs(this.y - pos.y);
  2560.         return absY <= dist &&
  2561.             absX <= dist &&
  2562.             (Math.pow(absY, 2) + Math.pow(absX, 2) <= Math.pow(dist, 2) * 2);
  2563.     }
  2564.  
  2565.     inRange3D(pos, dist) {
  2566.         const absX = Math.abs(this.x - pos.x);
  2567.         const absY = Math.abs(this.y - pos.y);
  2568.         const absZ = Math.abs(this.z - pos.z);
  2569.         return absY <= dist &&
  2570.             absX <= dist &&
  2571.             absZ <= dist &&
  2572.             (Math.pow(absY, 2) + Math.pow(absX, 2) + Math.pow(absZ, 2) <= Math.pow(dist, 2) * 3);
  2573.     }
  2574. }
  2575.  
  2576. class SColor {
  2577.     constructor(red = 0, green = 0, blue = 0) {
  2578.         this.red = red;
  2579.         this.green = green;
  2580.         this.blue = blue;
  2581.     }
  2582. }
  2583.  
  2584. class SLightDef {
  2585.     constructor() {
  2586.         this.flags = new LightFlags(0);    // LIGHT_x
  2587.         this.multiplier = 0;               // Multiplier
  2588.         this.pos = new S3DPoint();         // Position of light
  2589.         this.color = new SColor();         // RGB Color of light
  2590.         this.intensity = 0;                // Intensity of light
  2591.         this.lightindex = 0;               // Light index for 3d system
  2592.         this.lightid = 0;                  // Light id for dls system
  2593.     }
  2594. }
  2595.  
  2596. class ObjectFlags {
  2597.     constructor(value) {
  2598.         // Convert number to 32-bit binary string
  2599.         const bits = (value >>> 0).toString(2).padStart(32, '0');
  2600.  
  2601.         this.of_immobile = !!parseInt(bits[31 - 0]);      // Not affected by gravity etc
  2602.         this.of_editorlock = !!parseInt(bits[31 - 1]);    // Object is locked down (can't move in editor)
  2603.         this.of_light = !!parseInt(bits[31 - 2]);         // Object generates light (a light is on for object)
  2604.         this.of_moving = !!parseInt(bits[31 - 3]);        // Object is a moving object (characters, exits, players, missiles, etc.)
  2605.         this.of_animating = !!parseInt(bits[31 - 4]);     // Has animating imagery (animator pointer is set)
  2606.         this.of_ai = !!parseInt(bits[31 - 5]);            // Object has A.I.
  2607.         this.of_disabled = !!parseInt(bits[31 - 6]);      // Object A.I. is disabled
  2608.         this.of_invisible = !!parseInt(bits[31 - 7]);     // Not visible in map pane during normal play
  2609.         this.of_editor = !!parseInt(bits[31 - 8]);        // Is editor only object
  2610.         this.of_drawflip = !!parseInt(bits[31 - 9]);      // Reverse on the horizontal
  2611.         this.of_seldraw = !!parseInt(bits[31 - 10]);      // Editor is manipulating object
  2612.         this.of_reveal = !!parseInt(bits[31 - 11]);       // Player needs to see behind object (shutter draw)
  2613.         this.of_kill = !!parseInt(bits[31 - 12]);         // Suicidal (tells system to kill object next frame)
  2614.         this.of_generated = !!parseInt(bits[31 - 13]);    // Created by map generator
  2615.         this.of_animate = !!parseInt(bits[31 - 14]);      // Call the objects Animate() func AND create object animators
  2616.         this.of_pulse = !!parseInt(bits[31 - 15]);        // Call the object Pulse() function
  2617.         this.of_weightless = !!parseInt(bits[31 - 16]);   // Object can move, but is not affected by gravity
  2618.         this.of_complex = !!parseInt(bits[31 - 17]);      // Object is a complex object
  2619.         this.of_notify = !!parseInt(bits[31 - 18]);       // Notify object of a system change (see notify codes below)
  2620.         this.of_nonmap = !!parseInt(bits[31 - 19]);       // Not created, deleted, saved, or loaded by map (see below)
  2621.         this.of_onexit = !!parseInt(bits[31 - 20]);       // Object is currently on an exit (used to prevent exit loops)
  2622.         this.of_pause = !!parseInt(bits[31 - 21]);        // Script is paused
  2623.         this.of_nowalk = !!parseInt(bits[31 - 22]);       // Don't use walk map for this tile
  2624.         this.of_paralize = !!parseInt(bits[31 - 23]);     // Freeze the object in mid-animation
  2625.         this.of_nocollision = !!parseInt(bits[31 - 24]);  // Let the object go through boundries
  2626.         this.of_iced = !!parseInt(bits[31 - 25]);         // Used to know when to end the iced effect
  2627.     }
  2628.     // NOTE: OF_NONMAP
  2629.     // ----------------
  2630.     //
  2631.     // OF_NONMAP tells the map system that this object is managed outside of the regular map
  2632.     // system.  This object will not be LOADED, SAVED, CREATED, or DELETED by the map or
  2633.     // sector system.  Any object with this flag can be inserted into the map and assume that
  2634.     // it won't be deleted by the map system. This flag is intended for players, but can be used for
  2635.     // other objects.
  2636. }
  2637.  
  2638. async function main() {
  2639.     const gameDir = path.join('_INSTALLED_GAME', 'Revenant');
  2640.     const mapDir = path.join(gameDir, 'Modules', 'Ahkuilon', 'Map');
  2641.     const resourcesDir = path.join(gameDir, 'Resources');
  2642.  
  2643.     try {
  2644.         // Build the file cache
  2645.         console.log('Building file cache...');
  2646.         await DatParser.buildFileCache(resourcesDir);
  2647.  
  2648.         // Load IMAGERY.DAT first
  2649.         console.log('Loading IMAGERY.DAT...');
  2650.         const imageryDatPath = path.join(resourcesDir, 'imagery.dat');
  2651.         const imageryData = await ImageryDatParser.loadFile(imageryDatPath, gameDir);
  2652.         if (imageryData) {
  2653.             console.log(`Loaded ${imageryData.entries.length} imagery entries`);
  2654.             debugger;
  2655.         }
  2656.  
  2657.         // Then proceed with the rest of the processing
  2658.         await DatParser.loadClassDefinitions(gameDir);
  2659.  
  2660.         const files = await fs.readdir(mapDir);
  2661.         const datFiles = files.filter(file => file.toLowerCase().endsWith('.dat'));
  2662.  
  2663.         for (const datFile of datFiles) {
  2664.             const filePath = path.join(mapDir, datFile);
  2665.             console.log(`Processing ${datFile}...`);
  2666.             const result = await DatParser.loadFile(filePath, gameDir);
  2667.             if (result && result.numObjects) {
  2668.                 console.log(`File: ${datFile}`);
  2669.                 console.log('Version:', result.version);
  2670.                 console.log('Number of objects:', result.numObjects);
  2671.  
  2672.                 // Process each object's resource
  2673.                 for (const obj of result.objects) {
  2674.                     // todo
  2675.                 }
  2676.  
  2677.                 console.log('-------------------');
  2678.             }
  2679.         }
  2680.     } catch (error) {
  2681.         debugger;
  2682.         console.error('Error reading directory:', error);
  2683.     }
  2684. }
  2685.  
  2686. main().catch(console.error);
  2687.  
Tags: revenant
Add Comment
Please, Sign In to add comment