[Scummvm-git-logs] scummvm master -> dd36e0cf63da4ae39b6427a15211245d11a04e4d
Quote58
noreply at scummvm.org
Wed Apr 5 21:04:54 UTC 2023
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
7b343e667d IMMORTAL: Tile -> RoomTile
02bfa90db2 IMMORTAL: Add Kilobyte enum to utilities.h
7dc88a72d1 IMMORTAL: Rewrite compression.cpp to be accurate
dd36e0cf63 IMMORTAL: Implement mungeCBM() and the associated functions in drawChr.cpp
Commit: 7b343e667d1e19b4d03228f169a82130576cc54d
https://github.com/scummvm/scummvm/commit/7b343e667d1e19b4d03228f169a82130576cc54d
Author: Quote58 (michael.hayman54 at gmail.com)
Date: 2023-04-05T17:02:09-04:00
Commit Message:
IMMORTAL: Tile -> RoomTile
Changed paths:
engines/immortal/room.h
diff --git a/engines/immortal/room.h b/engines/immortal/room.h
index 89950412e28..a54f4f1dde0 100644
--- a/engines/immortal/room.h
+++ b/engines/immortal/room.h
@@ -38,7 +38,7 @@
namespace Immortal {
-enum Tile : uint8 {
+enum RoomTile : uint8 {
kTileFloor,
kTileUpper5,
kTileUpper3,
Commit: 02bfa90db25000dcbb04801042bcdfe91e516a9f
https://github.com/scummvm/scummvm/commit/02bfa90db25000dcbb04801042bcdfe91e516a9f
Author: Quote58 (michael.hayman54 at gmail.com)
Date: 2023-04-05T17:02:09-04:00
Commit Message:
IMMORTAL: Add Kilobyte enum to utilities.h
Changed paths:
engines/immortal/utilities.h
diff --git a/engines/immortal/utilities.h b/engines/immortal/utilities.h
index d95d473a168..9761649dc30 100644
--- a/engines/immortal/utilities.h
+++ b/engines/immortal/utilities.h
@@ -26,6 +26,18 @@
namespace Immortal {
+// The source uses nK many times throughout, which seems to be a compiler macro for n * 1024, ie. Kb
+enum Kilobyte {
+ k1K = 0x400, // 1024
+ k2K = 0x800, // 2048
+ k3K = 0xC00, // 3072
+ k4K = 0x1000, // 4096
+ k6K = 0x1800, // 6144
+ k8K = 0x2000, // 8192
+ k10K = 0x2800, // 10240
+ k16K = 0x4000 // 16384
+};
+
enum BitMask16 : uint16 {
kMaskLow = 0x00FF,
kMaskHigh = 0xFF00,
@@ -34,7 +46,6 @@ enum BitMask16 : uint16 {
kMaskHLow = 0x0F00,
kMaskLHigh = 0x00F0,
kMaskNeg = 0x8000,
- kMask12Bit = 0x0F9F // Compression code (pos, 00, len) is stored in lower 12 bits of word
};
enum BitMask8 : uint8 {
Commit: 7dc88a72d1f5326e15b5712f978a380758804bad
https://github.com/scummvm/scummvm/commit/7dc88a72d1f5326e15b5712f978a380758804bad
Author: Quote58 (michael.hayman54 at gmail.com)
Date: 2023-04-05T17:02:09-04:00
Commit Message:
IMMORTAL: Rewrite compression.cpp to be accurate
Changed paths:
engines/immortal/compression.cpp
diff --git a/engines/immortal/compression.cpp b/engines/immortal/compression.cpp
index 11ecaf95152..d0f32fd14b5 100644
--- a/engines/immortal/compression.cpp
+++ b/engines/immortal/compression.cpp
@@ -29,14 +29,42 @@
*/
namespace Immortal {
-Common::SeekableReadStream *ImmortalEngine::unCompress(Common::File *src, int srcLen) {
+enum codeMask {
+ kMaskMSBS = 0xF000, // Code link is Most significant bits
+ kMaskLSBS = 0xFF00, // K link is Least significant bits
+ kMaskCode = 0x0FFF // Code is 12 bit
+};
+
+Common::SeekableReadStream *ImmortalEngine::unCompress(Common::File *source, int lSource) {
/* Note: this function does not seek() in the file, which means
* that if there is a header on the data, the expectation is that
* seek() was already used to move past the header before this function.
*/
+ /* Other notes:
+ * Tk is k in (w,k)
+ * Link is spread out between code and tk, where code has the most significant 4 bits, and tk has the least significant 8
+ * Codes contains the keys (plus link codes) for the substring values of the dictionary and can be up to 12 bits (4096 total entries) in size
+ * Tk contains byte values from the compressed data (plus link codes)
+ * Stack contains the currently being recreated string before it gets sent to the output
+ */
+
+ // In the source, the data allocated here is a pointer passed to the function, but it's only used by this anyway
+ uint16 *pCodes = (uint16 *)malloc(k8K); // The Codes stack has 8 * 1024 bytes allocated
+ uint16 *pTk = (uint16 *)malloc(k8K); // The Tk has 8 * 1024 bytes allocated
+ uint16 pStack[k8K]; // In the source, the stack has the rest of the 20K. That's way more than it needs though, so we're just giving it 8k for now
+
+ uint16 oldCode = 0;
+ uint16 finChar = 0;
+ uint16 topStack = 0;
+ uint16 evenOdd = 0;
+ uint16 myCode = 0;
+ uint16 inCode = 0;
+ uint16 findEmpty = 0;
+ uint16 index = 0;
+
// If the source data has no length, we certainly do not want to decompress it
- if (srcLen == 0) {
+ if (lSource == 0) {
return nullptr;
}
@@ -44,216 +72,244 @@ Common::SeekableReadStream *ImmortalEngine::unCompress(Common::File *src, int sr
* We do not want it to be deleted from scope, as this location is where
* the readstream being returned will point to.
*/
- Common::MemoryWriteStreamDynamic dstW(DisposeAfterUse::NO);
-
- // The 20k bytes of memory that compression gets allocated to work with for the dictionary and the stack of chars
- uint16 start[0x4000]; // Really needs a better name, remember to do this future me
- uint16 ptk[0x4000]; // Pointer To Keys? Also needs a better name
- byte stack[0x4000]; // Stack of chars to be stored
-
- // These are the main variables we'll need for this
- uint16 findEmpty;
- uint16 code; // Needs to be ASL to index with
- uint16 inputCode;
- uint16 finalChar;
- uint16 myCode; // Silly name is silly
- uint16 oldCode;
- uint16 index; // The Y register was used to index the byte array's, this will sort of take its place
- uint16 evenOdd = 0;
- uint16 topStack = 0;
-
- byte outByte; // If only we could SEP #$20 like the 65816
-
- setupDictionary(start, ptk, findEmpty); // Clear the dictionary and also set findEmpty to 8k
- bool carry = true; // This will represent the carry flag so we can make this a clean loop
-
- code = getInputCode(carry, src, srcLen, evenOdd); // Get the first code
- if (carry == false) {
- return nullptr; // This is essentially the same as the first error check, but the source returns an error code and didn't even check it here so we might as well
- }
-
- finalChar = code;
- oldCode = code;
- myCode = code;
-
- outByte = code & kMaskLow;
- dstW.writeByte(outByte); // Take just the lower byte and write it the output
-
- // :nextcode
- while (carry == true) {
-
- code = getInputCode(carry, src, srcLen, evenOdd); // Get the next code
- if (carry == true) {
-
- index = code << 1;
- inputCode = code;
- myCode = code;
+ Common::MemoryWriteStreamDynamic dest(DisposeAfterUse::NO);
- // Split up the conditional statement to be easier to follow
- uint16 cond;
- cond = start[index] & kMaskLast;
- cond |= ptk[index];
+ /* In the source we save a backup of the starting pointer to the destination, which is increased
+ * as more data is added to it, so that the final length can be dest - destBkp. However in
+ * our case, the MemoryReadStream already has a size associated with it.
+ */
- if ((cond & kMaskHigh) == 0) { // Empty code
- index = topStack;
- outByte = finalChar & kMaskLow;
- stack[index] = outByte;
- topStack++;
- myCode = oldCode;
- }
+ // Clear the dictionary
+ setUpDictionary(pCodes, pTk, findEmpty);
+ evenOdd = 0;
+ topStack = 0;
+
+ // Get the initial input (always 0?)
+ inputCode(finChar, lSource, source, evenOdd);
+ oldCode = finChar;
+ myCode = oldCode;
+
+ // (byte) is basically the same as the SEP #$20 : STA : REP #$20
+ dest.writeByte((byte)myCode);
+
+ // Loops until it gets no more input codes (ie. length of source is 0)
+ while (inputCode(inCode, lSource, source, evenOdd) == 0) {
+ myCode = inCode;
+
+ // The source uses the Y register for this
+ // We can rearrange this a little to avoid using an extra variable, but for now we're pretending index is the register
+ index = inCode;
+
+ /* Check if the code is defined (has links for the linked list).
+ * We do this by grabbing the link portion from the code,
+ * then adding the Tk, and grabbing just the link portion.
+ * This way, if either of the link codes exists, we know it's defined,
+ * otherwise you just get zeros.
+ * This special case is for a string which is the same as the last string,
+ * but with the first char duplicated and added to the end (how common can that possibly be??)
+ */
+ if ((((pCodes[index] & kMaskMSBS) | pTk[index]) & kMaskLSBS) == 0) {
+ // Push the last char of this string, which is the same as the first of the previous one
+ pStack[topStack] = finChar;
+ topStack++;
+ myCode = oldCode;
+ }
- // :nextsymbol
- index = myCode << 1;
- while (index >= 0x200) {
- myCode = start[index] & kMask12Bit;
- outByte = ptk[index] & kMaskLow;
- index = topStack;
- stack[index] = outByte;
- topStack++;
- index = myCode << 1;
- }
+ // The code is defined, but it could be either a single char or a multi char
+ // If the index into the dictionary is above 100, it's a multi character substring
+ while ((myCode) >= 0x100) {
+ index = myCode;
+ myCode = pCodes[index] & kMaskCode;
+ pStack[topStack] = pTk[index] & kMaskLow;
+ topStack++;
+ }
- // :singlechar
- finalChar = (myCode >> 1);
- outByte = finalChar & kMaskLow;
- dstW.writeByte(outByte);
+ // Otherwise, it's a single char
+ finChar = myCode;
- // :dump
- while (topStack != 0xFFFF) { // Dump the chars on the stack into the output file
- outByte = stack[topStack] & kMaskLow;
- dstW.writeByte(outByte);
- topStack--;
- }
+ // which we write to the output
+ dest.writeByte((byte)myCode);
- topStack = 0;
- code = getMember(oldCode, finalChar, findEmpty, start, ptk);
- oldCode = inputCode;
+ // Dump the stack
+ index = topStack;
+ index--;
+ while (index < 0x8000) {
+ dest.writeByte((byte)pStack[index]);
+ index--;
}
+ topStack = 0;
+
+ // Hash the old code with the current char, if it isn't in the dictionary, append it
+ member(oldCode, finChar, pCodes, pTk, findEmpty, index);
+ // Set up the current code as the old code for the next code
+ oldCode = inCode;
}
/* Return a readstream with a pointer to the data in the write stream.
* This one we do want to dispose after using, because it will be in the scope of the engine itself
*/
- return new Common::MemoryReadStream(dstW.getData(), dstW.size(), DisposeAfterUse::YES);
+ return new Common::MemoryReadStream(dest.getData(), dest.size(), DisposeAfterUse::YES);
+
}
-void ImmortalEngine::setupDictionary(uint16 start[], uint16 ptk[], uint16 &findEmpty) {
- // Clear the whole dictionary
- for (int i = 0x3FFF; i >= 0; i--) {
- start[i] = 0;
- ptk[i] = 0;
+/* Clear the tables and mark the first 256 bytes of the char table as used */
+void ImmortalEngine::setUpDictionary(uint16 *pCodes, uint16 *pTk, uint16 &findEmpty) {
+ // Clear the tables completely (4095 entries, same as the mask for codes)
+ for (int i = kMaskCode; i >= 0; i -= 1) {
+ pCodes[i] = 0;
+ pTk[i] = 0;
}
- // Set the initial 256 bytes to be value 256, these are the characters without extensions
- for (int i = 255; i >= 0; i--) {
- ptk[i] = 256;
+ // Mark the first 0x100 as used for uncompress
+ for (int i = 0xFF; i >= 0; i -= 1) {
+ pTk[i] = 0x100;
}
- // This shouldn't really be done inside the function, but for the sake of consistency with the source, we will
- findEmpty = 0x8000;
+ // findEmpty is a pointer for finding empty slots, so it starts at the end of the data (data is 2 bytes wide, so it's 4k instead of 8k)
+ findEmpty = k4K;
}
-int ImmortalEngine::getInputCode(bool &carry, Common::File *src, int &srcLen, uint16 &evenOdd) {
- // Check if we're at the end of the file
- if (srcLen == 0) {
- carry = false;
- return 0;
+/* Get a code from the input stream. 1 = no more codes, 0 = got code
+ * On even iterations, we grab the first word.
+ * On odd iterations, we grab the word starting from the second byte of the previous word
+ */
+int ImmortalEngine::inputCode(uint16 &outCode, int &lSource, Common::File *source, uint16 &evenOdd) {
+ // If length is 0, we're done getting codes
+ if (lSource == 0) {
+ // No more codes
+ return 1;
}
- uint16 c;
- if (evenOdd != 0) { // Odd
- srcLen--;
+ // Even
+ if (evenOdd == 0) {
+ lSource -= 2; // Even number of bytes, decrease by 2
+ evenOdd++; // Next alignment will be odd
+
+ /* The codes are stored in 12 bits, so 3 bytes = 2 codes
+ * nnnn nnnn [nnnn cccc cccc cccc] & 0x0FFF
+ * nnnn nnnn [0000 cccc cccc cccc]
+ */
+ outCode = source->readUint16LE() & kMaskCode;
+ source->seek(-1, SEEK_CUR);
+
+ // Odd
+ } else {
+ lSource--;
evenOdd--;
- c = (src->readUint16BE() >> 3) & 0x00FE; // & #-1-1
- } else { // Even
- srcLen -= 2;
- evenOdd++;
- c = (src->readUint16BE() & kMask12Bit) << 1;
- src->seek(-1, SEEK_CUR);
+ /* This grabs the next code which is made up of the previous code's second byte
+ * plus the current code's byte + the next 2 byte value
+ * [nnnn nnnn nnnn cccc] cccc cccc >> 3
+ * [000n nnnn nnnn nnnc] cccc cccc & 0xFFFE <- this is done so the Y register has code * 2
+ * [000n nnnn nnnn nnn0] cccc cccc >> 1 <- in our case, we could have just done code >> 4
+ * [0000 nnnn nnnn nnnn]
+ */
+ outCode = ((source->readUint16LE() >> 3) & 0xFFFE) >> 1;
}
- return c;
-}
-uint16 ImmortalEngine::getMember(uint16 codeW, uint16 k, uint16 &findEmpty, uint16 start[], uint16 ptk[]) {
- // This function is effectively void, as the return value is only used in compression
-
- // k and codeW are local variables with the value of oldCode and finalChar
+ // We have a good code, no error
+ return 0;
+}
- uint16 hash;
- uint16 tmp;
- bool ag = true;
+int ImmortalEngine::member(uint16 &codeW, uint16 &k, uint16 *pCodes, uint16 *pTk, uint16 &findEmpty, uint16 &index) {
+ // Step one is to make a hash value out of W (oldCode) and k (finChar)
+ index = ((((((k << 3) ^ k) << 1) ^ k) ^ codeW));
- hash = (k << 3) ^ k;
- hash = (hash << 1) ^ codeW;
- hash <<= 1;
+ // The hash value has to be larger than 200 because that's where the single chars are
+ if (index < 0x100) {
+ index += 0x100;
+ }
- hash = (hash >= 0x200) ? hash : hash + 0x200;
+ if ((((pCodes[index] & kMaskMSBS) | pTk[index]) & kMaskLSBS) == 0) {
+ // There was no link, so we insert the key, mark the table as used, with no link
+ pCodes[index] = codeW;
+ pTk[index] = k | 0x100;
- uint16 a = start[hash] & 0x0F00;
- uint16 b = ptk[hash] & kMaskHigh;
- if (a | b) {
- start[hash] = codeW;
- ptk[hash] = k | 0x100;
- return ptk[hash];
+ // Code not found, return error
+ return 1;
}
- // This loop is a bit wacky, due to the way the jumps were stuctured in the source
- while (ag == true) {
- if ((start[hash] & kMask12Bit) == codeW) {
- if ((ptk[hash] & kMaskLow) == k) {
- return hash >> 1;
+ // There is a link, so it's not empty
+ // This is a bad loop, because there is no safe way out if the data isn't right, but it's the source logic
+ // If there is anything corrupted in the data, the game will get stuck forever
+ while (true) {
+ uint16 tmp = 0;
+
+ // If the code matches
+ if ((pCodes[index] & kMaskCode) == codeW) {
+ // And k also matches
+ if ((pTk[index] & kMaskLow) == k) {
+ // Then entry is found, return no error
+ return 0;
}
}
- tmp = start[hash] & kMaskLast;
- if (tmp == 0) {
- // I've separated this into it's own function for the sake of this loop being readable
- appendList(codeW, k, hash, findEmpty, start, ptk, tmp);
- ag = false;
-
- } else {
- hash = xba(ptk[hash]);
- hash = (hash & kMaskLow) | (tmp >> 4);
- hash <<= 1;
- }
- }
- return hash;
-}
-
-void ImmortalEngine::appendList(uint16 codeW, uint16 k, uint16 &hash, uint16 &findEmpty, uint16 start[], uint16 ptk[], uint16 &tmp) {
- uint16 prev;
- uint16 link;
-
- prev = hash;
- if (hash >= 0x200) {
- setupDictionary(start, ptk, findEmpty);
-
- } else {
- bool found = false;
- while (found == false) {
- hash -= 2;
- if (hash >= 0x200) {
- setupDictionary(start, ptk, findEmpty);
- found = true;
+ // Entry was used, but it is not holding the desired key
+ // Follow link to next entry, if there is no next entry, append to the list
+ if ((pCodes[index] & kMaskMSBS) == 0) {
+ // Find an empty entry and link it to the last entry in the chain, then put the data in the new entry
+ uint16 prev = index;
+ if (findEmpty >= 0x100) {
+ // Table is not full, keep looking
+ do {
+ findEmpty--;
+ // This is slightly more redundant than the source, but I trust the compiler to add a branch here
+ if (findEmpty < 0x100) {
+ setUpDictionary(pCodes, pTk, findEmpty);
+ return 1;
+ }
+ // We decrease the index and check the entry until we find an empty entry or the end of the table
+ } while ((((pCodes[findEmpty] & kMaskMSBS) | pTk[findEmpty]) & kMaskLSBS) != 0);
+
+ // The link is zero, therefor we have found an empty entry
+
+ pCodes[findEmpty] = codeW;
+ pTk[findEmpty] = k | 0x100; // Marked as used, but still no link because this is the end of the list
+
+ // Now we attach a link to this entry from the previous one in the list
+ uint16 link = findEmpty;
+
+ // Get the link of this entry
+ /* 0000 llll llll llll xba
+ * llll llll 0000 llll & kMaskLSBS
+ * llll llll 0000 0000
+ */
+ tmp = xba(link) & kMaskLSBS;
+
+ // On the previous entry, take out the K and add the new link onto it
+ /* xxxx xxxx xxxx xxxx & kMaskLow
+ * 0000 0000 xxxx xxxx | tmp
+ * llll llll xxxx xxxx
+ */
+ pTk[prev] = (pTk[prev] & kMaskLow) | tmp;
+
+ // And now the code gets it's half of the link written in
+ /* 0000 llll llll llll << 4
+ * llll llll llll llll & kMaskMSBS
+ * llll 0000 0000 0000 | pCodes[Prev]
+ * llll xxxx xxxx xxxx
+ */
+ pCodes[prev] = ((link << 4) & kMaskMSBS) | pCodes[prev];
+
+ // Done
+ return 1;
+
+ } else {
+ // Table is full, reset dictionary
+ setUpDictionary(pCodes, pTk, findEmpty);
+ return 1;
}
- // Split up the conditional statement to be easier to follow
- uint16 cond;
- cond = start[hash] & kMaskLast;
- cond |= ptk[hash];
-
- if ((cond & kMaskHigh) == 0) {
- findEmpty = hash;
- start[hash] = codeW;
- ptk[hash] = k | 0x100;
-
- link = hash >> 1;
-
- ptk[prev] = (link << 8) | (ptk[prev] & kMaskLow);
- start[prev] |= (link >> 4) & kMaskLast;
- found = true;
- }
+ } else {
+ // Put the link code together by combining the MSBS of the code and the LSBS of k
+ /* code = l000 >> 4
+ * = 0l00
+ * k = ll00 xba
+ * = 00ll
+ * k | code = 0lll
+ */
+ tmp = (pCodes[index] & kMaskMSBS) >> 4;
+ index = ((xba(pTk[index]) & kMaskLow) | tmp);// << 1;
}
}
}
Commit: dd36e0cf63da4ae39b6427a15211245d11a04e4d
https://github.com/scummvm/scummvm/commit/dd36e0cf63da4ae39b6427a15211245d11a04e4d
Author: Quote58 (michael.hayman54 at gmail.com)
Date: 2023-04-05T17:02:09-04:00
Commit Message:
IMMORTAL: Implement mungeCBM() and the associated functions in drawChr.cpp
Changed paths:
engines/immortal/drawChr.cpp
engines/immortal/immortal.h
engines/immortal/kernal.cpp
diff --git a/engines/immortal/drawChr.cpp b/engines/immortal/drawChr.cpp
index 5f8e4e8dff9..01336a13f58 100644
--- a/engines/immortal/drawChr.cpp
+++ b/engines/immortal/drawChr.cpp
@@ -21,26 +21,345 @@
#include "immortal/immortal.h"
+/* -- How does tile image construction work --
+ * Tiles in the source are quite complex, and the way they are drawn
+ * to the screen makes use of some very low level opcode trickery.
+ * To that end, here is the data structure in the source:
+ * Files: .CNM (Character Number Map), .UNV (Universe)
+ * .CNM = Logical CNM, uncompressed
+ * .UNV = Universe parameters, uncompressed + CNM/CBM, compressed
+ * Logical CNM = Draw type info about the CNM (indexes into a table used when constructing the complete CNM)
+ * Universe parameters = Bounds, animations (unused), Columns, Rows, Chrs, Cells
+ * CNM = Map of all cells in the level, with each cell being an index to the Chr in the CBM
+ * CBM = All gfx data for the level, stored in 'Chrs' which are 4x2 tiles, with each tile being 2x2 blocks of 8x8 pixel gfx data,
+ * stored in Linear Reversed Chunk gfx, *not* bitplane data.
+ *
+ * Data Structures: CNM, Solid, Left, Right, Draw
+ * CNM = Unchanged from the uncompressed file data
+ * Solid/Left/Right = [2 bytes] index for screen clipping, [2 bytes] index to position in Draw where the chr gfx start
+ * Draw = Series of microroutines that use PEA to move pixel data directly to screen buffer. For Left/Right versions,
+ * the routine draws only half of the tile data, divided by a diagonal in one of ULHC/LLHC/URHC/LRHC
+ *
+ * Tile Structures:
+ * Chr = This term is used for multiple things throughout the source, but in this context it is the entire 8x4 set of 8x8 pixel gfx data
+ * Block = Not a term used in the source, but is what describes the 8x8 pixel gfx data
+ *
+ * So the way this works is that after uncompressing the CNM and CBM data,
+ * the game converts the pixel data into the microroutines (mungeX()) and builds the Solid/Left/Right routines.
+ * Then, when tile drawing needs to happen, the drawing routines (drawX()) will take the chr as an index into Solid/Left/Right,
+ * and use the clip data to find a starting scanline, which it can use to determine where the stack will point to.
+ * With the stack pointing to the screen buffer, the game will jump to the microroutine (called Linear Coded Chr Routines in the source)
+ * and execute the code, moving the pixel data to the screen.
+ *
+ * So how does it change for this port?
+ * Well we can't replicate that PEA trick, so we'll have to ditch the microroutines. However,
+ * we will keep the same general structure. Instead of converting the pixel data to routines,
+ * we instead convert it into the type of pixel data needed by the ScummVM screen buffer,
+ * ie. change 1 pixel per nyble to 1 pixel per byte. However, it gets more complicated in how
+ * it has to be stored. In the source, reading the pixel data doesn't have to account for how
+ * many pixels to read per scanline, because it is executing code which will end the scanline
+ * whenever it has been written to do so. In our case, we must instead rely on the pixel reading
+ * function to read the same number of pixels per scanline that we put into the data structure
+ * when converting it from the raw pixel data.
+ * So now we have:
+ * Draw: A vector of Chrs
+ * Chr: A set of 32 scan lines
+ * Scanline: A byte buffer containing anywhere from 1 to 32 bytes of pixel data
+ */
+
namespace Immortal {
-int ImmortalEngine::mungeCBM() {
+void ImmortalEngine::drawSolid(int chr, int x, int y) {
+ /* Okay, this routine is quite different here compared to
+ * the source, so I'll explain:
+ * At this point in the source, you had an array of linear
+ * coded chr routines that draw pixels by directly storing
+ * byte values to the screen buffer. These routines would
+ * get executed from these drawX() routines. It didn't need
+ * to worry about how many pixels per line for example,
+ * because the code would simply stop sending pixels to the
+ * screen when it reached the end of the scanline (this is
+ * important for the other drawX() routines). In our case,
+ * we instead have a table of structs that contain an array
+ * of scan lines, each with 1 - 32 bytes of pixel data
+ * that is already formatted for use by ScummVM's screen buffer.
+ * These drawX() functions must now draw the pixels from that
+ * data.
+ */
+
+ // This will need clipping later
+ int index = _Solid[chr];
+ int point = (y * kResH) + x;
+
+ // For every scanline, draw the pixels and move down by the size of the screen
+ for (int cy = 0; cy < 32; cy++, point += kResH) {
+
+ // For solid, we always have 64 pixels in each scanline
+ for (int cx = 0; cx < 64; cx++) {
+ _screenBuff[point + cx] = _Draw[index]._scanlines[cy][cx];
+ }
+ }
+}
+
+void ImmortalEngine::drawLRHC(int chr, int x, int y) {
+ // This will need clipping later
+ int index = _Right[chr];
+ int point = (y * kResH) + x;
+
+ for (int cy = 0; cy < 32; cy++, point += kResH) {
+
+ // We only want to draw the amount of pixels based on the number of lines down the tile
+ for (int cx = 0; cx < (2 * (cy + 1)); cx++) {
+ _screenBuff[point + cx + (64 - (2 * (cy + 1)))] = _Draw[index]._scanlines[cy][cx];
+ }
+ }
+}
+
+void ImmortalEngine::drawLLHC(int chr, int x, int y) {
+ // This will need clipping later
+ int index = _Left[chr];
+ int point = (y * kResH) + x;
+
+ for (int cy = 0; cy < 32; cy++, point += kResH) {
+ for (int cx = 0; cx < (2 * (cy + 1)); cx++) {
+ _screenBuff[point + cx] = _Draw[index]._scanlines[cy][cx];
+ }
+ }
+}
+
+void ImmortalEngine::drawULHC(int chr, int x, int y) {
+ // This will need clipping later
+ int index = _Right[chr];
+ int point = (y * kResH) + x;
+
+ for (int cy = 0; cy < 32; cy++, point += kResH) {
+ for (int cx = 0; cx < (64 - (cy * 2)); cx++) {
+ _screenBuff[point + cx] = _Draw[index]._scanlines[cy][cx];
+ }
+ }
+}
+
+void ImmortalEngine::drawURHC(int chr, int x, int y) {
+ // This will need clipping later
+ int index = _Left[chr];
+ int point = (y * kResH) + x;
+
+ for (int cy = 0; cy < 32; cy++, point += kResH) {
+ for (int cx = 0; cx < (64 - (cy * 2)); cx++) {
+ _screenBuff[point + cx + (cy * 2)] = _Draw[index]._scanlines[cy][cx];
+ }
+ }
+}
+
+int ImmortalEngine::mungeCBM(uint16 num2Chrs) {
+ const uint16 kTBlisterCorners[60] = {7, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 3, 5, 3, 5, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 16, 16, 16, 16, 8,
+ 8, 8, 8, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+ };
+
+ // Is this missing an entry? There should be 20 columns, and this is only 19...
+ const uint16 kTLogicalCorners[19] = {1, 1, 1, 1, 16, 8, 1, 8,
+ 16, 1, 1, 8, 1, 16, 8, 16,
+ 1, 16, 8
+ };
+
+ // Each tile is 1024 bytes, so the oldCBM is 1024 * number of tiles
+ int lCBM = k1K * _univ->_numChrs;
+ _oldCBM = (byte *)malloc(lCBM);
+
+ //debug("Length of CBM: %d", lCBM);
+
+ // Now we get the CBM from the file
+ _dataBuffer->seek(_univ->_num2Cells);
+ _dataBuffer->read(_oldCBM, lCBM);
+
+ // And now we need to set up the data structures that will be used to expand the CNM/CBM
+ // Each Chr needs a 'solid' function, but some also need a 'left' and 'right' function as well
+ // So each one needs to be the size of as many Chrs as you have
+ _Solid = (uint16 *)malloc(num2Chrs);
+ _Right = (uint16 *)malloc(num2Chrs);
+ _Left = (uint16 *)malloc(num2Chrs);
+
+ // _Draw is actually going to be a length that depends on the CNM, so we need it to be a vector
+
+ // In the source, this does all 3 at once, but we have them in separate variables
+ uint16 *lists[3] = {_Solid, _Right, _Left};
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < _univ->_numChrs; j++) {
+ lists[i][j] = 0;
+ }
+ }
- return 0;//_CNM - pDraw;
+ uint16 cell = 0;
+ uint16 chr = 0;
+ uint16 corners = 0;
+ int oldChr = 0;
+ uint16 drawIndex = 0;
+
+ // Loop over every cell in the CNM, and set up the draw routines for each draw type needed
+ do {
+ // The CNM has the chr number in each cell
+ chr = _CNM[cell];
+
+ // Have we already done it?
+ if (_Solid[chr] == 0) {
+
+ // Mark it as done even if not solid presumably for future checks on the solid chr list?
+ _Solid[chr] = 1;
+
+ // If cell is within the first 3 rows, and cell 17 is 0
+ if ((cell >= (_univ->_numCols)) && (cell < (_univ->_numCols * 3)) && (_CNM[17] != 0)) {
+ // Then we get it from a table because this is the water level
+ corners = kTBlisterCorners[cell];
+
+ } else {
+ // Otherwise we get it from a table indexed by the entry in the logical CNM
+ corners = kTLogicalCorners[_logicalCNM[cell]];
+ }
+
+ // In the source this is actually asl 5 : asl + rol 5, but we can just use an int instead of 2 uint16s
+ oldChr = chr * 1024;
+
+ // Corners determines whether we create a _Draw entry for just solid, or diagonals as well
+ if ((corners & 1) != 0) {
+ storeAddr(_Solid, chr, drawIndex);
+ mungeSolid(oldChr, drawIndex);
+ }
+
+ if ((corners & 2) != 0) {
+ storeAddr(_Left, chr, drawIndex);
+ mungeLLHC(oldChr, drawIndex);
+ }
+
+ if ((corners & 4) != 0) {
+ storeAddr(_Right, chr, drawIndex);
+ mungeLRHC(oldChr, drawIndex);
+ }
+
+ if ((corners & 8) != 0) {
+ storeAddr(_Left, chr, drawIndex);
+ mungeURHC(oldChr, drawIndex);
+ }
+
+ if ((corners & 16) != 0) {
+ storeAddr(_Right, chr, drawIndex);
+ mungeULHC(oldChr, drawIndex);
+ }
+ }
+
+ cell++;
+ } while (cell != (_univ->_num2Cells / 2));
+
+ // Finally just return the size of the draw table, which is essentially the expanded CBM
+ return _Draw.size();
+}
+
+void ImmortalEngine::storeAddr(uint16 *drawType, uint16 chr, uint16 drawIndex) {
+ // The entry at chr2 is the index into the draw table
+ // In the source this required checking bank boundries, luckily that's not relevant here
+ drawType[chr] = drawIndex;
}
-void ImmortalEngine::storeAddr() {
+void ImmortalEngine::mungeSolid(int oldChr, uint16 &drawIndex) {
+ // We need a Chr for the entry in the draw table
+ Chr chrData;
+
+ // This is a little different from the source, because the source creates the linear coded chr routines
+ // So here we are just grabbing the pixel data in the normal way
+
+ // For every line of pixels in the chr
+ for (int py = 0; py < 32; py++) {
+ // Each scanline needs a byte buffer
+ byte *scanline = (byte *)malloc(64);
+
+ // For every pixel in the line, we extract the data from the oldCBM
+ for (int px = 0; px < 64; px += 2) {
+ /* Pixels are stored in Linear Reversed Chunk format
+ * Which is 2 pixels per byte, stored in reverse order.
+ */
+ scanline[px] = (_oldCBM[oldChr] & kMask8High) >> 4;
+ scanline[px + 1] = (_oldCBM[oldChr] & kMask8Low);
+ oldChr++;
+ }
+ // Now we add the byte buffer into the chrData
+ chrData._scanlines[py] = scanline;
+ }
+ // And we add the chrData into the draw table
+ _Draw.push_back(chrData);
+ drawIndex++;
}
-void ImmortalEngine::mungeSolid() {}
-void ImmortalEngine::mungeLRHC() {}
-void ImmortalEngine::mungeLLHC() {}
-void ImmortalEngine::mungeULHC() {}
-void ImmortalEngine::mungeURHC() {}
-void ImmortalEngine::drawSolid(int chr, int x, int y) {}
-void ImmortalEngine::drawULHC(int chr, int x, int y) {}
-void ImmortalEngine::drawURHC(int chr, int x, int y) {}
-void ImmortalEngine::drawLLHC(int chr, int x, int y) {}
-void ImmortalEngine::drawLRHC(int chr, int x, int y) {}
+void ImmortalEngine::mungeLRHC(int oldChr, uint16 &drawIndex) {
+ Chr chrData;
+
+ for (int py = 0; py < 32; py++) {
+ byte *scanline = (byte *)malloc(2 * (py + 1));
+ oldChr += (32 - (py + 1));
+
+ for (int px = 0; px < (2 * (py + 1)); px += 2) {
+ scanline[px] = (_oldCBM[oldChr] & kMask8High) >> 4;
+ scanline[px + 1] = (_oldCBM[oldChr] & kMask8Low);
+ oldChr++;
+ }
+ chrData._scanlines[py] = scanline;
+ }
+ _Draw.push_back(chrData);
+ drawIndex++;
+}
+
+void ImmortalEngine::mungeLLHC(int oldChr, uint16 &drawIndex) {
+ Chr chrData;
+
+ for (int py = 0; py < 32; py++) {
+ byte *scanline = (byte *)malloc(2 * (py + 1));
+
+ for (int px = 0; px < (2 * (py + 1)); px += 2) {
+ scanline[px] = (_oldCBM[oldChr] & kMask8High) >> 4;
+ scanline[px + 1] = (_oldCBM[oldChr] & kMask8Low);
+ oldChr++;
+ }
+ oldChr += (32 - (py + 1));
+ chrData._scanlines[py] = scanline;
+ }
+ _Draw.push_back(chrData);
+ drawIndex++;
+}
+
+void ImmortalEngine::mungeULHC(int oldChr, uint16 &drawIndex) {
+ Chr chrData;
+
+ for (int py = 0; py < 32; py++) {
+ byte *scanline = (byte *)malloc(64 - ((py + 1) * 2));
+
+ for (int px = 0; px < (64 - ((py + 1) * 2)); px += 2) {
+ scanline[px] = (_oldCBM[oldChr] & kMask8High) >> 4;
+ scanline[px + 1] = (_oldCBM[oldChr] & kMask8Low);
+ oldChr++;
+ }
+ oldChr += (py + 1);
+ chrData._scanlines[py] = scanline;
+ }
+ _Draw.push_back(chrData);
+ drawIndex++;
+}
+
+void ImmortalEngine::mungeURHC(int oldChr, uint16 &drawIndex) {
+ Chr chrData;
+
+ for (int py = 0; py < 32; py++) {
+ byte *scanline = (byte *)malloc(64 - (py * 2));
+
+ for (int px = 0; px < (64 - (py * 2)); px += 2) {
+ scanline[px] = (_oldCBM[oldChr] & kMask8High) >> 4;
+ scanline[px + 1] = (_oldCBM[oldChr] & kMask8Low);
+ oldChr++;
+ }
+ oldChr += (py + 1);
+ chrData._scanlines[py] = scanline;
+ }
+ _Draw.push_back(chrData);
+ drawIndex++;
+}
} // namespace Immortal
diff --git a/engines/immortal/immortal.h b/engines/immortal/immortal.h
index 7205d66a56c..c8063a054df 100644
--- a/engines/immortal/immortal.h
+++ b/engines/immortal/immortal.h
@@ -167,6 +167,10 @@ struct Univ {
uint16 _num2Chrs = 0;
};
+struct Chr {
+ byte *_scanlines[32];
+};
+
struct ImmortalGameDescription;
// Forward declaration because we will need the Disk and Room classes
@@ -214,7 +218,7 @@ public:
const int kScreenH__ = 128; // ???
const int kViewPortW = 256;
const int kViewPortH = 128;
- const int kScreenSize = (kResH *kResV) * 2; // The size of the screen buffer is (320x200) * 2 byte words
+ const int kScreenSize = (kResH * kResV) * 2; // The size of the screen buffer is (320x200) * 2 byte words
const uint16 kScreenLeft = 32;
const uint16 kScreenTop = 20;
const uint8 kTextLeft = 8;
@@ -252,16 +256,6 @@ public:
0, 0, 0, 0, 0, 0
};
- const uint16 kTBlisterCorners[60] = {7, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 3, 5, 3, 5, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 16, 16, 16, 16, 8,
- 8, 8, 8, 16, 16, 16, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
- };
-
- const uint16 kTLogicalCorners[19] = {1, 1, 1, 1, 16, 8, 1, 8,
- 16, 1, 1, 8, 1, 16, 8, 16,
- 1, 16, 8
- };
-
// Disk offsets
const int kPaletteOffset = 21205; // This is the byte position of the palette data in the disk
@@ -390,7 +384,7 @@ public:
// Asset members
int _numSprites = 0; // This is more accurately actually the index within the sprite array, so _numSprites + 1 is the current number of sprites
DataSprite _dataSprites[kFont + 1]; // All the sprite data, indexed by SpriteName
- Sprite _sprites[kMaxSprites]; // All the sprites shown on screen
+ Sprite _sprites[kMaxSprites]; // All the sprites shown on screen
Cycle _cycles[kMaxCycles];
Common::Array<Common::String> _strPtrs; // Str should really be a char array, but inserting frame values will be stupid so it's just a string instead
Common::Array<Motive> _motivePtrs;
@@ -401,15 +395,19 @@ public:
CArray2D<Motive> _programPtrs;
Common::Array<ObjType> _objTypePtrs;
- // Universe members in order of their original memory layout
- uint16 *_logicalCNM; // As confusing as this is, we get Logical CNM from the .CNM file, and we get the CNM from the .UNV file
+ // Universe members
+ Univ *_univ; // Pointer to the struct that contains the universe properties
+ uint16 *_logicalCNM; // Draw-type data for the CNM (indexes into )
+ uint16 *_CNM; // Stands for CHARACTER NUMBER MAP, but really it should be TILE NUMBER MAP, because it points to tiles, which are made of characters
+ byte *_oldCBM; // Stands for CHARACTER BIT MAP, but should probably be called like, TILE CHARACTER MAP, because it is the full gfx data for all tiles
+ Common::Array<Chr> _Draw; // In the source this contained the Linear Coded Chr Routines, but here it just contains the expanded pixel data
+ uint16 *_Solid;
+ uint16 *_Right;
+ uint16 *_Left;
+ Common::SeekableReadStream *_dataBuffer; // This contains the uncompressed CNM + CBM
+
uint16 *_modCNM;
uint16 *_modLogicalCNM;
- Univ *_univ; // Pointer to the struct that contains the universe properties
- Common::SeekableReadStream *_dataBuffer; // This contains the CNM and the CBM
- uint16 *_CNM; // Stands for CHARACTER NUMBER MAP
- byte *_CBM; // Stands for CHARACTER BIT MAP (?)
- byte *_oldCBM;
uint16 _myCNM[(kViewPortCW + 1)][(kViewPortCH + 1)];
uint16 _myModCNM[(kViewPortCW + 1)][(kViewPortCH + 1)];
@@ -417,6 +415,8 @@ public:
// Screen members
byte *_screenBuff; // The final buffer that will transfer to the screen
+ Graphics::Surface *_mainSurface; // The ScummVM Surface
+
uint16 _columnX[kViewPortCW + 1];
uint16 _columnTop[kViewPortCW + 1];
uint16 _columnIndex[kViewPortCW + 1]; // Why the heck is this an entire array, when it's just an index that gets zeroed before it gets used anyway...
@@ -435,7 +435,6 @@ public:
uint16 _myUnivPointX = 0;
uint16 _myUnivPointY = 0;
int _num2DrawItems = 0;
- Graphics::Surface *_mainSurface;
GenericSprite _genSprites[6];
// Palette members
@@ -447,7 +446,7 @@ public:
uint16 _palWhite[16];
uint16 _palBlack[16];
uint16 _palDim[16];
- byte _palRGB[48]; // Palette that ScummVM actually uses, which is an RGB conversion of the original
+ byte _palRGB[48]; // Palette that ScummVM actually uses, which is an RGB conversion of the original
/*
@@ -546,13 +545,13 @@ public:
*/
// Main
- int mungeCBM();
- void storeAddr();
- void mungeSolid();
- void mungeLRHC();
- void mungeLLHC();
- void mungeULHC();
- void mungeURHC();
+ int mungeCBM(uint16 num2Chrs);
+ void storeAddr(uint16 *drawType, uint16 chr2, uint16 drawIndex);
+ void mungeSolid(int oldChr, uint16 &drawIndex);
+ void mungeLRHC(int oldChr, uint16 &drawIndex);
+ void mungeLLHC(int oldChr, uint16 &drawIndex);
+ void mungeULHC(int oldChr, uint16 &drawIndex);
+ void mungeURHC(int oldChr, uint16 &drawIndex);
void drawSolid(int chr, int x, int y);
void drawULHC(int chr, int x, int y);
void drawURHC(int chr, int x, int y);
@@ -685,14 +684,12 @@ public:
*/
// Main routines
- Common::SeekableReadStream *unCompress(Common::File *src, int srcLen);
+ Common::SeekableReadStream *unCompress(Common::File *source, int lSource);
// Subroutines called by unCompress
- void setupDictionary(uint16 start[], uint16 ptk[], uint16 &findEmpty);
- int getInputCode(bool &carry, Common::File *src, int &srcLen, uint16 &evenOdd);
- uint16 getMember(uint16 codeW, uint16 k, uint16 &findEmpty, uint16 start[], uint16 ptk[]);
- void appendList(uint16 codeW, uint16 k, uint16 &hash, uint16 &findEmpty, uint16 start[], uint16 ptk[], uint16 &tmp);
-
+ void setUpDictionary(uint16 *pCodes, uint16 *pTk, uint16 &findEmpty);
+ int inputCode(uint16 &outCode, int &lSource, Common::File *source, uint16 &evenOdd);
+ int member(uint16 &codeW, uint16 &k, uint16 *pCodes, uint16 *pTk, uint16 &findEmpty, uint16 &index);
/*
* [door.cpp] Functions from Door.GS
diff --git a/engines/immortal/kernal.cpp b/engines/immortal/kernal.cpp
index 7fe0f711223..521fb8411cd 100644
--- a/engines/immortal/kernal.cpp
+++ b/engines/immortal/kernal.cpp
@@ -490,16 +490,15 @@ int ImmortalEngine::loadUniv(char mazeNum) {
// The logical CNM contains the contents of mazeN.CNM, with every entry being bitshifted left once
_logicalCNM = (uint16 *)malloc(mazeCNM->size());
mazeCNM->seek(0);
- mazeCNM->read(_logicalCNM, mazeCNM->size());
- for (int i = 0; i < (mazeCNM->size()); i++) {
- _logicalCNM[i] <<= 1;
+ for (int i = 0; i < (mazeCNM->size() / 2); i++) {
+ _logicalCNM[i] = mazeCNM->readUint16LE();
}
// This is where the source defines the location of the pointers for modCNM, lModCNM, and then the universe properties
// So in similar fasion, here we will create the struct for universe
_univ = new Univ();
- // Next we load the mazeN.UNV file, which contains the compressed data for multiple things
+ // Next we load the mazeN.UNV file, which contains the compressed data for the Univ, CNM, and CBM
Common::String sUNV = "MAZE" + Common::String(mazeNum) + ".UNV";
Common::SeekableReadStream *mazeUNV = loadIFF(sUNV);
if (!mazeUNV) {
@@ -508,8 +507,7 @@ int ImmortalEngine::loadUniv(char mazeNum) {
}
debug("Size of maze UNV: %ld", mazeUNV->size());
- // This is also where the pointer to CNM is defined, because it is 26 bytes after the pointer to Univ. However for our purposes
- // These are separate
+ // This is also where the pointer to CNM is defined, because it is 26 bytes after the pointer to Univ. However for our purposes these are separate
// After which, we set data length to be the total size of the file
lData = mazeUNV->size();
@@ -534,6 +532,7 @@ int ImmortalEngine::loadUniv(char mazeNum) {
// If there are animations (are there ever?), the univ data is expanded from 26 to include them
if (mazeUNV->readUint16LE() != 0) {
+ debug("there are animations??");
mazeUNV->seek(0x2C);
lStuff += mazeUNV->readUint16LE();
}
@@ -541,30 +540,35 @@ int ImmortalEngine::loadUniv(char mazeNum) {
// lData is everything from the .UNV file after the universe properties
lData -= lStuff;
- // At this point in the source, the data after universe properties is moved to the end of the heap
+ // At this point in the source, the data after universe properties is moved to the end of the heap where it can be uncompressed back to the CNM pointer
- // We then uncompress all of that data, into the place in the heap where the CNM is supposed to be (the Maze Heap)
+ // We then uncompress all of that data
mazeUNV->seek(lStuff);
- _dataBuffer = unCompress((Common::File *) mazeUNV, lData);
+ _dataBuffer = unCompress((Common::File *)mazeUNV, lData);
debug("size of uncompressed CNM/CBM data %ld", _dataBuffer->size());
- // Check every entry in the CNM, with the highest number being the highest number of chrs?
+ // Check every entry in the CNM (while we add them). The highest number is the total number of tiles in the file
+ _CNM = (uint16 *)malloc(_univ->_num2Cells);
_univ->_numChrs = 0;
_dataBuffer->seek(0);
- for (int i = 0; i < _univ->_num2Cells; i++) {
- uint16 chr = _dataBuffer->readUint16LE();
- if (chr >= _univ->_numChrs) {
- _univ->_numChrs = chr;
+
+ // The CNM is 0x500 bytes (usually), with each entry being a word, so we need 0x500 / 2
+ for (int i = 0; i < _univ->_num2Cells / 2; i++) {
+ _CNM[i] = _dataBuffer->readUint16LE();
+ if (_CNM[i] >= _univ->_numChrs) {
+ _univ->_numChrs = _CNM[i];
}
}
-
- _dataBuffer->seek(0);
- _univ->_numChrs++; // Inc one more time being 0 counts
+ _univ->_numChrs++; // The 0th tile is still a tile, so inc one more time to account for it
+ debug("Number of Chars: %d", _univ->_numChrs);
_univ->_num2Chrs = _univ->_numChrs << 1;
- //int lCNMCBM = mungeCBM(_univ->_num2Chrs);
- int lCNMCBM = mungeCBM();
+ // Set the databuffer back to position 0 for now. The remaining data in databuffer is the CBM (we don't really need to do this, but for clarity we will)
+ _dataBuffer->seek(0);
+ // In the source, this is where we munge the CBM, which is to say that we sort of combine the CBM into the CNM to create routines that draw tiles from their component characters, which are stored in sequence by tile
+ int lCNMCBM = mungeCBM(_univ->_num2Chrs);
+
debug("nchrs %04X, n2cells %04X, univX %04X, univY %04X, cols %04X, rows %04X, lstuff %04X", _univ->_numChrs, _univ->_num2Cells, _univ->_rectX, _univ->_rectY, _univ->_numCols, _univ->_numRows, lStuff);
// We don't actually want to blister any rooms yet, so we give it a POV of (0,0)
More information about the Scummvm-git-logs
mailing list