/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // Based on Phantasma code by Thomas Harte (2013), // available at https://github.com/TomHarte/Phantasma/ (MIT) #include "common/memstream.h" #include "common/file.h" #include "freescape/freescape.h" #include "freescape/language/8bitDetokeniser.h" #include "freescape/objects/connections.h" #include "freescape/objects/global.h" #include "freescape/objects/group.h" #include "freescape/objects/sensor.h" namespace Freescape { uint16 FreescapeEngine::readPtr(Common::SeekableReadStream *file) { uint16 value; if (isAmiga() || isAtariST()) { uint16 lo = file->readUint16BE(); assert(lo < 256); uint16 hi = file->readUint16BE(); assert(hi < 256); value = 256 * hi + lo; if (value >= 0xFFFF / 2) error("Failed to read pointer with value 0x%x", value); value = 2 * value; } else value = file->readUint16LE(); return value; } uint16 FreescapeEngine::readField(Common::SeekableReadStream *file, int bits) { uint16 value; assert(bits == 8 || bits == 16); if (isAmiga() || isAtariST()) { if (bits == 16) { value = file->readUint16BE(); } else { assert(bits == 8); value = file->readUint16BE(); if (value >= 256) { warning("failed to read byte with value 0x%x", value); value = value & 0xff; } } } else { if (bits == 8) value = file->readByte(); else value = file->readUint16LE(); } return value; } Common::Array FreescapeEngine::readArray(Common::SeekableReadStream *file, int size) { Common::Array array; for (int i = 0; i < size; i++) { if (isAmiga() || isAtariST()) { uint16 value = file->readUint16BE(); array.push_back(value); } else array.push_back(readField(file, 8)); } return array; } Group *FreescapeEngine::load8bitGroup(Common::SeekableReadStream *file, byte rawFlagsAndType) { if (isDark() || isEclipse()) return load8bitGroupV1(file, rawFlagsAndType); else return load8bitGroupV2(file, rawFlagsAndType); } Group *FreescapeEngine::load8bitGroupV1(Common::SeekableReadStream *file, byte rawFlagsAndType) { debugC(1, kFreescapeDebugParser, "Object of type 'group'"); Common::Array animation; Common::Array groupObjects = readArray(file, 3); Math::Vector3d offset1; Math::Vector3d offset2; for (int i = 0; i < 3; i++) { uint16 value = 0; if (isAmiga() || isAtariST()) value = readField(file, 16); else value = readField(file, 8); if (value > 127) value = value - 255; debugC(1, kFreescapeDebugParser, "Group offset[1][%d] = %d", i, value); offset1.setValue(i, value); } // object ID uint16 objectID = readField(file, 8); // size of object on disk; we've accounted for 8 bytes // already so we can subtract that to get the remaining // length beyond here uint8 byteSizeOfObject = readField(file, 8); debugC(1, kFreescapeDebugParser, "Raw object %d ; type group ; size %d", objectID, byteSizeOfObject); if (byteSizeOfObject < 9) { error("Not enough bytes %d to read object %d with type group", byteSizeOfObject, objectID); } assert(byteSizeOfObject >= 9); byteSizeOfObject = byteSizeOfObject - 9; for (int i = 0; i < 3; i++) { uint16 value = 0; if (isAmiga() || isAtariST()) value = readField(file, 16); else value = readField(file, 8); if (value > 127) value = value - 255; debugC(1, kFreescapeDebugParser, "Group offset[2][%d] = %d", i, value); offset2.setValue(i, value); } byteSizeOfObject = byteSizeOfObject - 3; for (int i = 0; i < 3; i++) debugC(1, kFreescapeDebugParser, "Group object[%d] = %d", i, groupObjects[i]); Common::Array groupOperations; Common::Array groupPositions; while (byteSizeOfObject > 0) { uint16 value = 0; if (isAmiga() || isAtariST()) value = readField(file, 16); else value = readField(file, 8); int opcode = value >> 8; debugC(1, kFreescapeDebugParser, "Reading opcode: %x", opcode); AnimationOpcode* operation = new AnimationOpcode(opcode); byteSizeOfObject--; if (opcode == 0xff) { assert(value == 0xffff); debugC(1, kFreescapeDebugParser, "Group operation rewind"); } else if (opcode == 0x01) { debugC(1, kFreescapeDebugParser, "Group operation script execution"); // get the length uint32 lengthOfCondition = value & 0xff; assert(lengthOfCondition > 0); debugC(1, kFreescapeDebugParser, "Length of condition: %d at %lx", lengthOfCondition, long(file->pos())); // get the condition Common::Array conditionArray = readArray(file, lengthOfCondition); operation->conditionSource = detokenise8bitCondition(conditionArray, operation->condition, isAmiga() || isAtariST()); debugC(1, kFreescapeDebugParser, "%s", operation->conditionSource.c_str()); byteSizeOfObject = byteSizeOfObject - lengthOfCondition; } else { if (byteSizeOfObject >= 1) { operation->position.x() = value & 0xff; operation->position.y() = file->readByte(); operation->position.z() = file->readByte(); debugC(1, kFreescapeDebugParser, "Group operation %d move to: %f %f %f", opcode, operation->position.x(), operation->position.y(), operation->position.z()); byteSizeOfObject = byteSizeOfObject - 1; } else { debugC(1, kFreescapeDebugParser, "Incomplete group operation %d", opcode); byteSizeOfObject = 0; delete operation; continue; } } animation.push_back(operation); } return new Group( objectID, rawFlagsAndType, groupObjects, offset1, offset2, animation); } Group *FreescapeEngine::load8bitGroupV2(Common::SeekableReadStream *file, byte rawFlagsAndType) { debugC(1, kFreescapeDebugParser, "Object of type 'group'"); Common::Array animation; Common::Array groupObjects = readArray(file, 3); Math::Vector3d offset1; Math::Vector3d offset2; for (int i = 0; i < 3; i++) { int16 value = 0; if (isAmiga() || isAtariST()) value = readField(file, 16); else value = readField(file, 8); if (value > 127) value = value - 255; debugC(1, kFreescapeDebugParser, "Group offset[1][%d] = %d", i, value); offset1.setValue(i, value); } // object ID uint16 objectID = readField(file, 8); // size of object on disk; we've accounted for 8 bytes // already so we can subtract that to get the remaining // length beyond here uint8 byteSizeOfObject = readField(file, 8); debugC(1, kFreescapeDebugParser, "Raw object %d ; type group ; size %d", objectID, byteSizeOfObject); if (byteSizeOfObject < 9) { error("Not enough bytes %d to read object %d with type group", byteSizeOfObject, objectID); } assert(byteSizeOfObject >= 9); byteSizeOfObject = byteSizeOfObject - 9; for (int i = 0; i < 3; i++) { int16 value = 0; if (isAmiga() || isAtariST()) value = readField(file, 16); else value = readField(file, 8); if (value > 127) value = value - 255; debugC(1, kFreescapeDebugParser, "Group offset[2][%d] = %d", i, value); offset2.setValue(i, value); } byteSizeOfObject = byteSizeOfObject - 3; for (int i = 0; i < 3; i++) debugC(1, kFreescapeDebugParser, "Group object[%d] = %d", i, groupObjects[i]); Common::Array groupOperations; Common::Array groupPositions; while (byteSizeOfObject > 0) { uint16 opcode = 0; if (isAmiga() || isAtariST()) opcode = readField(file, 16); else opcode = readField(file, 8); AnimationOpcode* operation = new AnimationOpcode(opcode); byteSizeOfObject--; if (opcode == 0x80) { debugC(1, kFreescapeDebugParser, "Group operation rewind"); } else if (opcode == 0x01) { debugC(1, kFreescapeDebugParser, "Group operation script execution"); // get the length uint32 lengthOfCondition = readField(file, 8); assert(lengthOfCondition > 0); byteSizeOfObject--; debugC(1, kFreescapeDebugParser, "Length of condition: %d at %lx", lengthOfCondition, long(file->pos())); // get the condition Common::Array conditionArray = readArray(file, lengthOfCondition); operation->conditionSource = detokenise8bitCondition(conditionArray, operation->condition, isAmiga() || isAtariST()); debugC(1, kFreescapeDebugParser, "%s", operation->conditionSource.c_str()); byteSizeOfObject = byteSizeOfObject - lengthOfCondition; } else { if (byteSizeOfObject >= 3) { operation->position.x() = readField(file, 8); operation->position.y() = readField(file, 8); operation->position.z() = readField(file, 8); debugC(1, kFreescapeDebugParser, "Group operation %d move to: %f %f %f", opcode, operation->position.x(), operation->position.y(), operation->position.z()); byteSizeOfObject = byteSizeOfObject - 3; } else { byteSizeOfObject = 0; delete operation; continue; } } animation.push_back(operation); } return new Group( objectID, rawFlagsAndType, groupObjects, offset1, offset2, animation); } Object *FreescapeEngine::load8bitObject(Common::SeekableReadStream *file) { byte rawFlagsAndType = readField(file, 8); debugC(1, kFreescapeDebugParser, "Raw object data flags and type: %d", rawFlagsAndType); ObjectType objectType = (ObjectType)(rawFlagsAndType & 0x1F); if (objectType == ObjectType::kGroupType) return load8bitGroup(file, rawFlagsAndType); Math::Vector3d position, v; position.x() = readField(file, 8); position.y() = readField(file, 8); position.z() = readField(file, 8); v.x() = readField(file, 8); v.y() = readField(file, 8); v.z() = readField(file, 8); // object ID uint16 objectID = readField(file, 8); if (objectID == 224 && (rawFlagsAndType & 0x1F) == 29) // If objectType is out of range, fix it objectType = (ObjectType)7; // size of object on disk; we've accounted for 8 bytes // already so we can subtract that to get the remaining // length beyond here uint8 byteSizeOfObject = readField(file, 8); debugC(1, kFreescapeDebugParser, "Raw object %d ; type %d ; size %d", objectID, (int)objectType, byteSizeOfObject); if (byteSizeOfObject < 9) { error("Not enough bytes %d to read object %d with type %d", byteSizeOfObject, objectID, objectType); } if (objectType > ObjectType::kGroupType && isDemo() && isCastle()) { // Castle DOS demo has an invalid object, which should not be parsed. debugC(1, kFreescapeDebugParser, "WARNING: invalid object %d!", objectID); readArray(file, byteSizeOfObject - 9); return nullptr; } if (isC64() && isDark()) objectType = (ObjectType)(objectType % ObjectType::kGroupType); assert(objectType <= ObjectType::kGroupType); assert(byteSizeOfObject >= 9); byteSizeOfObject = byteSizeOfObject - 9; if (objectID == 255 && objectType == ObjectType::kEntranceType) { debugC(1, kFreescapeDebugParser, "Found the room structure (objectID: 255 with size %d)", byteSizeOfObject + 6); Common::Array structureArray; structureArray.push_back(uint8(position.x())); structureArray.push_back(uint8(position.y())); structureArray.push_back(uint8(position.z())); structureArray.push_back(uint8(v.x())); structureArray.push_back(uint8(v.y())); structureArray.push_back(uint8(v.z())); byteSizeOfObject++; while(--byteSizeOfObject > 0) structureArray.push_back(readField(file, 8)); return new GlobalStructure(structureArray); } else if (objectID == 254 && objectType == ObjectType::kEntranceType) { debugC(1, kFreescapeDebugParser, "Found the area connections (objectID: 254 with size %d)", byteSizeOfObject + 6); Common::Array connectionsArray; connectionsArray.push_back(uint8(position.x())); connectionsArray.push_back(uint8(position.y())); connectionsArray.push_back(uint8(position.z())); connectionsArray.push_back(uint8(v.x())); connectionsArray.push_back(uint8(v.y())); connectionsArray.push_back(uint8(v.z())); byteSizeOfObject++; while(--byteSizeOfObject > 0) connectionsArray.push_back(readField(file, 8)); return new AreaConnections(connectionsArray); } debugC(1, kFreescapeDebugParser, "Object %d ; type %d ; size %d", objectID, (int)objectType, byteSizeOfObject); debugC(1, kFreescapeDebugParser, "pos: %f %f %f", position.x(), position.y(), position.z()); switch (objectType) { default: { debugC(1, kFreescapeDebugParser, "size: %f %f %f", v.x(), v.y(), v.z()); // read the appropriate number of colours int numberOfColours = GeometricObject::numberOfColoursForObjectOfType(objectType); Common::Array *colours = new Common::Array; Common::Array *ecolours = nullptr; if (!isDriller() && (isAmiga() || isAtariST())) ecolours = new Common::Array; debugC(1, kFreescapeDebugParser, "Number of colors: %d", numberOfColours / 2); uint8 entry; for (uint8 colour = 0; colour < numberOfColours / 2; colour++) { uint8 data = 0; uint8 extraData = 0; if (!isDriller() && (isAmiga() || isAtariST())) { uint16 field = file->readUint16BE(); data = field & 0xff; extraData = field >> 8; } else data = readField(file, 8); entry = data & 0xf; colours->push_back(entry); debugC(1, kFreescapeDebugParser, "color[%d] = %x", 2 * colour, entry); if (!isDriller() && (isAmiga() || isAtariST())) { ecolours->push_back(extraData & 0xf); debugC(1, kFreescapeDebugParser, "ecolor[%d] = %x", 2 * colour, extraData & 0xf); } entry = data >> 4; colours->push_back(entry); debugC(1, kFreescapeDebugParser, "color[%d] = %x", 2 * colour + 1, entry); if (!isDriller() && (isAmiga() || isAtariST())) { ecolours->push_back(extraData >> 4); debugC(1, kFreescapeDebugParser, "ecolor[%d] = %x", 2 * colour + 1, extraData >> 4); } byteSizeOfObject--; } // read extra vertices if required... int numberOfOrdinates = GeometricObject::numberOfOrdinatesForType(objectType); debugC(1, kFreescapeDebugParser, "number of ordinates %d", numberOfOrdinates); Common::Array *ordinates = nullptr; if (numberOfOrdinates) { ordinates = new Common::Array; uint16 ord = 0; if (byteSizeOfObject < numberOfOrdinates) { error("Not enough bytes to read all the ordinates"); // file->seek(byteSizeOfObject, SEEK_CUR); // return nullptr; } for (int ordinate = 0; ordinate < numberOfOrdinates; ordinate++) { ord = readField(file, 8); debugC(1, kFreescapeDebugParser, "ord: %x", ord); ordinates->push_back(32 * ord); byteSizeOfObject--; } } // grab the object condition, if there is one FCLInstructionVector instructions; Common::String conditionSource; if (byteSizeOfObject) { Common::Array conditionArray = readArray(file, byteSizeOfObject); conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST()); // instructions = getInstructions(conditionSource); debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str()); } debugC(1, kFreescapeDebugParser, "End of object at %lx", long(file->pos())); // create an object return new GeometricObject( objectType, objectID, rawFlagsAndType, // flags 32 * position, 32 * v, // size colours, ecolours, ordinates, instructions, conditionSource); } break; case kEntranceType: { debugC(1, kFreescapeDebugParser, "rotation: %f %f %f", v.x(), v.y(), v.z()); FCLInstructionVector instructions; Common::String conditionSource; if (byteSizeOfObject > 0) { if (!isCastle()) { debugC(1, kFreescapeDebugParser, "Warning: extra %d bytes in entrance", byteSizeOfObject); while (byteSizeOfObject--) debugC(1, kFreescapeDebugParser, "b: %x", readField(file, 8)); } else { Common::Array conditionArray = readArray(file, byteSizeOfObject); conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST()); debugC(1, kFreescapeDebugParser, "Entrance condition:"); debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str()); } byteSizeOfObject = 0; } assert(byteSizeOfObject == 0); debugC(1, kFreescapeDebugParser, "End of object at %lx", long(file->pos())); if (isCastle()) { if (position.x() == 255) position.x() = -8096; else position.x() = 32 * position.x(); if (position.y() == 255) position.y() = -8096; else position.y() = 32 * position.y(); if (position.z() == 255) position.z() = -8096; else position.z() = 32 * position.z(); if (v.x() == 255 && v.y() == 255 && v.z() == 255) { v.x() = -8096; v.y() = -8096; v.z() = -8096; } else { v.x() = 5 * v.x(); v.y() = 5 * v.y(); v.z() = 5 * v.z(); } } else { v = 5 * v; position = 32 * position; } // create an entrance return new Entrance( objectID, position, v, // rotation instructions, conditionSource); } break; case kSensorType: { debugC(1, kFreescapeDebugParser, "rotation: %f %f %f", v.x(), v.y(), v.z()); FCLInstructionVector instructions; Common::String conditionSource; if (isCastle()) { // TODO assert(byteSizeOfObject == 0); return new Sensor( objectID, 32 * position, 5 * v, 0, 0, 0, 0, rawFlagsAndType, instructions, conditionSource); } assert(byteSizeOfObject >= 5); byte color = readField(file, 8) & 0xf; assert(color > 0); byte firingInterval = readField(file, 8); uint16 firingRange = readPtr(file) / 2; if (isDark()) firingRange = firingRange / 2; byte sensorAxis = readField(file, 8); byteSizeOfObject = byteSizeOfObject - 5; debugC(1, kFreescapeDebugParser, "Sensor detected with firing interval %d, firing range %d and axis %d", firingInterval, firingRange, sensorAxis); // grab the object condition, if there is one if (byteSizeOfObject) { Common::Array conditionArray = readArray(file, byteSizeOfObject); conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST()); debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str()); } debugC(1, kFreescapeDebugParser, "End of object at %lx", long(file->pos())); // create an entrance return new Sensor( objectID, 32 * position, 5 * v, // rotation? color, firingInterval, firingRange, sensorAxis, rawFlagsAndType, instructions, conditionSource); } break; case kGroupType: error("Unreachable"); break; } // Unreachable } static const char *eclipseRoomName[] = { "* SAHARA", "HORAKHTY", "NEPHTHYS", "KHEPRESH", " RAMESES", "PHARAOHS", " SHABAKA", "ILLUSION", "????????" }; static const char *eclipse2RoomName[] = { "\" SAHARA", "ENTRANCE", "\" SPHINX", "\"SELQUET", "\" OSIRIS", "\" THEBES", "\" BEHBET", "\"'l JINX", "GAME OVER", }; Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 ncolors) { Common::String name; uint32 base = file->pos(); debugC(1, kFreescapeDebugParser, "Area base: %x", base); uint8 areaFlags = readField(file, 8); uint8 numberOfObjects = readField(file, 8); uint8 areaNumber = readField(file, 8); uint16 cPtr = readPtr(file); debugC(1, kFreescapeDebugParser, "Condition pointer: %x", cPtr); uint8 scale = readField(file, 8); debugC(1, kFreescapeDebugParser, "Scale: %d", scale); uint8 skyColor = areaFlags & 15; uint8 groundColor = areaFlags >> 4; if (groundColor == 0) groundColor = 255; uint8 usualBackgroundColor = 0; uint8 underFireBackgroundColor = 0; uint8 paperColor = 0; uint8 inkColor = 0; if (!(isCastle() && (isSpectrum() || isCPC() || isC64()))) { usualBackgroundColor = readField(file, 8); underFireBackgroundColor = readField(file, 8); paperColor = readField(file, 8); inkColor = readField(file, 8); } else { uint8 attribute = readField(file, 8); debugC(1, kFreescapeDebugParser, "Attribute: %x", attribute); if (isSpectrum()) { paperColor = attribute >> 4; inkColor = attribute & 0xf; } else if (isCPC()) { paperColor = attribute; inkColor = 0xb; } skyColor = 0; } debugC(1, kFreescapeDebugParser, "Colors usual background: %d", usualBackgroundColor); debugC(1, kFreescapeDebugParser, "Colors under fire background: %d", underFireBackgroundColor); debugC(1, kFreescapeDebugParser, "Color Paper: %d", paperColor); debugC(1, kFreescapeDebugParser, "Color Ink: %d", inkColor); debugC(1, kFreescapeDebugParser, "Additional colors: %d %d", skyColor, groundColor); debugC(1, kFreescapeDebugParser, "Area %d", areaNumber); debugC(1, kFreescapeDebugParser, "Flags: %d Objects: %d", areaFlags, numberOfObjects); // debug("Condition Ptr: %x", cPtr); debugC(1, kFreescapeDebugParser, "Pos before first object: %lx", long(file->pos())); // Driller specific uint8 gasPocketX = 0; uint8 gasPocketY = 0; uint8 gasPocketRadius = 0; // Castle specific uint8 extraColor[4] = {}; if (isEclipse()) { byte idx = readField(file, 8); if (isEclipse2()) { name = idx < 8 ? eclipse2RoomName[idx] : eclipse2RoomName[8]; } else name = idx < 8 ? eclipseRoomName[idx] : eclipseRoomName[8]; name = name + "-" + char(readField(file, 8)) + " "; int i = 0; while (i < 3) { name = name + char(readField(file, 8)); i++; } if (isAmiga() || isAtariST()) { groundColor = skyColor; skyColor = 0; } } else if (isDriller() || isDark()) { if (isDriller()) { gasPocketX = readField(file, 8); gasPocketY = readField(file, 8); gasPocketRadius = readField(file, 8); } else { name = name + char(readField(file, 8)); name = name + char(readField(file, 8)); name = name + char(readField(file, 8)); } debugC(1, kFreescapeDebugParser, "Gas pocket at (%d, %d) with radius %d", gasPocketX, gasPocketY, gasPocketRadius); int i = 0; while (i < 12) { name = name + char(readField(file, 8)); i++; } } else if (isCastle()) { byte idx = readField(file, 8); if (isAmiga()) name = _messagesList[idx + 51]; else if (isSpectrum() || isCPC()) name = areaNumber == 255 ? "GLOBAL" : _messagesList[idx + 16]; else name = _messagesList[idx + 41]; if (isDOS()) { extraColor[0] = readField(file, 8); extraColor[1] = readField(file, 8); extraColor[2] = readField(file, 8); extraColor[3] = readField(file, 8); debugC(1, kFreescapeDebugParser, "Extra colors: %x %x %x %x", extraColor[0], extraColor[1], extraColor[2], extraColor[3]); } if (isAmiga()) { extraColor[0] = readField(file, 8); extraColor[1] = readField(file, 8); extraColor[2] = readField(file, 8); extraColor[3] = readField(file, 8); // TODO groundColor = skyColor; skyColor = 0; debugC(1, kFreescapeDebugParser, "Extra colors: %x %x %x %x", extraColor[0], extraColor[1], extraColor[2], extraColor[3]); } } debugC(1, kFreescapeDebugParser, "Area name: %s", name.c_str()); ObjectMap *objectsByID = new ObjectMap; ObjectMap *entrancesByID = new ObjectMap; for (uint8 object = 0; object < numberOfObjects && areaNumber != 192; object++) { debugC(1, kFreescapeDebugParser, "Reading object: %d", object); Object *newObject = load8bitObject(file); if (newObject) { newObject->scale(scale); if (newObject->getType() == kEntranceType) { if (entrancesByID->contains(newObject->getObjectID() & 0x7fff)) error("WARNING: replacing object id %d (%d)", newObject->getObjectID(), newObject->getObjectID() & 0x7fff); (*entrancesByID)[newObject->getObjectID() & 0x7fff] = newObject; } else { if (objectsByID->contains(newObject->getObjectID())) error("WARNING: replacing object id %d", newObject->getObjectID()); (*objectsByID)[newObject->getObjectID()] = newObject; } } else if (!(isDemo() && isCastle())) error("Failed to read an object!"); } // Link all groups if (areaNumber != 255) { // Do not link objects in the room structure for (auto &obj : *objectsByID) { if (obj._value->getType() == ObjectType::kGroupType) { Group *group = (Group *)obj._value; for (auto &objInner : *objectsByID) group->linkObject(objInner._value); } } } int64 endLastObject = file->pos(); debugC(1, kFreescapeDebugParser, "Last position %" PRIx64, endLastObject); debugC(1, kFreescapeDebugParser, "endLastObject is supposed to be %x", base + cPtr); if ((isDark() || isEclipse()) && (isAmiga() || isAtariST())) assert(endLastObject <= static_cast(base + cPtr) + 4); else assert(endLastObject == static_cast(base + cPtr) || areaNumber == 192); file->seek(base + cPtr); uint8 numConditions = readField(file, 8); debugC(1, kFreescapeDebugParser, "%d area conditions at %x of area %d", numConditions, base + cPtr, areaNumber); Area *area = new Area(areaNumber, areaFlags, objectsByID, entrancesByID, isCastle()); area->_name = name; area->_scale = scale; area->_skyColor = skyColor; area->_groundColor = groundColor; area->_inkColor = inkColor; area->_paperColor = paperColor; area->_usualBackgroundColor = usualBackgroundColor; area->_underFireBackgroundColor = underFireBackgroundColor; area->_extraColor[0] = extraColor[0]; area->_extraColor[1] = extraColor[1]; area->_extraColor[2] = extraColor[2]; area->_extraColor[3] = extraColor[3]; // Driller specific area->_gasPocketPosition = Common::Point(32 * gasPocketX, 32 * gasPocketY); area->_gasPocketRadius = 32 * gasPocketRadius; while (numConditions--) { FCLInstructionVector instructions; // get the length uint32 lengthOfCondition = readField(file, 8); debugC(1, kFreescapeDebugParser, "length of condition: %d", lengthOfCondition); // get the condition if (lengthOfCondition > 0) { Common::Array conditionArray = readArray(file, lengthOfCondition); Common::String conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST()); area->_conditions.push_back(instructions); area->_conditionSources.push_back(conditionSource); debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str()); } } debugC(1, kFreescapeDebugParser, "End of area at %lx", long(file->pos())); return area; } void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offset, int ncolors) { file->seek(offset); uint8 numberOfAreas = readField(file, 8); if (isAmiga() && isCastle() && isDemo()) numberOfAreas = 87; debugC(1, kFreescapeDebugParser, "Number of areas: %d", numberOfAreas); uint32 dbSize = readField(file, 16); debugC(1, kFreescapeDebugParser, "Database ends at %x", dbSize); if (isAmiga() || isAtariST()) debugC(1, kFreescapeDebugParser, "Extra field: %x", readField(file, 16)); uint8 startArea = readField(file, 8); debugC(1, kFreescapeDebugParser, "Start area: %d", startArea); uint8 startEntrance = readField(file, 8); debugC(1, kFreescapeDebugParser, "Entrace area: %d", startEntrance); uint8 initialEnergy1 = 0; uint8 initialShield1 = 0; uint8 initialEnergy2 = 0; uint8 initialShield2 = 0; if (isCastle() && (isSpectrum() || isCPC())) { initialShield1 = readField(file, 8); } else { readField(file, 8); // Unknown initialEnergy1 = readField(file, 8); initialShield1 = readField(file, 8); initialEnergy2 = readField(file, 8); initialShield2 = readField(file, 8); } debugC(1, kFreescapeDebugParser, "Initial levels of energy: %d and shield: %d", initialEnergy1, initialShield1); debugC(1, kFreescapeDebugParser, "Initial levels of energy: %d and shield: %d", initialEnergy2, initialShield2); if (isCastle() && (isSpectrum() || isCPC())) file->seek(offset + 0x6); else if (isAmiga() || isAtariST()) file->seek(offset + 0x14); else if (isDriller() && isC64()) file->seek(offset + 0x2a); else file->seek(offset + 0xa); debugC(1, kFreescapeDebugParser, "Color map:"); uint8 data; for (int i = 0; i < 15; i++) { byte *entry = (byte *)malloc(4 * sizeof(byte)); data = readField(file, 8); *entry = data; entry++; debugC(1, kFreescapeDebugParser, "%x", data); data = readField(file, 8); *entry = data; entry++; debugC(1, kFreescapeDebugParser, "%x", data); data = readField(file, 8); *entry = data; entry++; debugC(1, kFreescapeDebugParser, "%x", data); data = readField(file, 8); *entry = data; debugC(1, kFreescapeDebugParser, "%x", data); debugC(1, kFreescapeDebugParser, "---"); _colorMap.push_back(entry - 3); } if (isCastle() && (isSpectrum() || isCPC())) file->seek(offset + 0x42); else if (isAmiga() || isAtariST()) file->seek(offset + 0x8c); else file->seek(offset + 0x46); uint16 demoDataTable; demoDataTable = readPtr(file); debugC(1, kFreescapeDebugParser, "Pointer to demo data: %x\n", demoDataTable); uint16 globalByteCodeTable; globalByteCodeTable = readPtr(file); debugC(1, kFreescapeDebugParser, "GBCT: %x\n", globalByteCodeTable); if (isDOS()) loadDemoData(file, offset + demoDataTable, 128); // TODO: check this size file->seek(offset + globalByteCodeTable); debugC(1, kFreescapeDebugParser, "Position: %lx\n", long(file->pos())); uint8 numConditions = readField(file, 8); debugC(1, kFreescapeDebugParser, "%d global conditions", numConditions); while (numConditions--) { // TODO: read global conditions in Amiga FCLInstructionVector instructions; // get the length uint32 lengthOfCondition = readField(file, 8); debugC(1, kFreescapeDebugParser, "length of condition: %d at %lx", lengthOfCondition, long(file->pos())); // get the condition if (lengthOfCondition > 0) { Common::Array conditionArray = readArray(file, lengthOfCondition); Common::String conditionSource = detokenise8bitCondition(conditionArray, instructions, isAmiga() || isAtariST()); _conditions.push_back(instructions); _conditionSources.push_back(conditionSource); debugC(1, kFreescapeDebugParser, "%s", conditionSource.c_str()); } } if (isDriller() || isDark()) { debugC(1, kFreescapeDebugParser, "Time to finish the game:"); if (isAmiga() || isAtariST()) file->seek(offset + 0x168); else file->seek(offset + 0xb4); Common::String n; if (isDriller()) { n += char(readField(file, 8)); n += char(readField(file, 8)); debugC(1, kFreescapeDebugParser, "'%s' hours", n.c_str()); _initialCountdown =_initialCountdown + 3600 * atoi(n.c_str()); n.clear(); n += char(readField(file, 8)); assert(n == ":"); n.clear(); } n += char(readField(file, 8)); n += char(readField(file, 8)); debugC(1, kFreescapeDebugParser, "'%s' minutes", n.c_str()); _initialCountdown = _initialCountdown + 60 * atoi(n.c_str()); n.clear(); n += char(readField(file, 8)); assert(n == ":"); n.clear(); n += char(readField(file, 8)); n += char(readField(file, 8)); debugC(1, kFreescapeDebugParser, "'%s' seconds", n.c_str()); _initialCountdown = _initialCountdown + atoi(n.c_str()); if (_useExtendedTimer) _initialCountdown = 359999; // 99:59:59 } else if (isCastle()) _initialCountdown = 1000000000; else if (isEclipse()) _initialCountdown = 7200; // 02:00:00 if (isCastle() && (isSpectrum() || isCPC())) file->seek(offset + 0x4f); else if (isAmiga() || isAtariST()) file->seek(offset + 0x190); else file->seek(offset + 0xc8); debugC(1, kFreescapeDebugParser, "areas index at: %lx", long(file->pos())); uint16 *fileOffsetForArea = new uint16[numberOfAreas]; for (uint16 area = 0; area < numberOfAreas; area++) { fileOffsetForArea[area] = readPtr(file); debugC(1, kFreescapeDebugParser, "offset: %x", fileOffsetForArea[area]); } // grab the areas Area *newArea = nullptr; for (uint16 area = 0; area < numberOfAreas; area++) { debugC(1, kFreescapeDebugParser, "Starting to parse area index %d at offset %x", area, fileOffsetForArea[area]); file->seek(offset + fileOffsetForArea[area]); newArea = load8bitArea(file, ncolors); if (newArea) { if (!_areaMap.contains(newArea->getAreaID())) _areaMap[newArea->getAreaID()] = newArea; else debugC(1, kFreescapeDebugParser, "WARNING: area ID repeated: %d", newArea->getAreaID()); } else { error("Invalid area %d?", area); } } delete[] fileOffsetForArea; if (!_areaMap.contains(startArea)) _startArea = newArea->getAreaID(); else _startArea = startArea; _startEntrance = startEntrance; _colorNumber = ncolors; _binaryBits = 8; } void FreescapeEngine::loadFonts(Common::SeekableReadStream *file, int offset) { Common::Array chars; if (isAmiga() || isAtariST()) chars = getCharsAmigaAtari(file, offset, 85); else if (isC64()) { Common::Array rawChars = getChars(file, offset, 85); Common::Rect charRect; for (Graphics::ManagedSurface *rawChar : rawChars) { charRect = Common::Rect(0, 0, 8, 6); Graphics::ManagedSurface *charSurface = new Graphics::ManagedSurface(); charSurface->create(8, 6, Graphics::PixelFormat::createFormatCLUT8()); charSurface->copyRectToSurface(*rawChar, 0, 0, charRect); chars.push_back(charSurface); charRect = Common::Rect(8, 0, 16, 6); charSurface = new Graphics::ManagedSurface(); charSurface->create(8, 6, Graphics::PixelFormat::createFormatCLUT8()); charSurface->copyRectToSurface(*rawChar, 0, 0, charRect); chars.push_back(charSurface); } } else chars = getChars(file, offset, 85); _font = Font(chars); if (_renderMode == Common::kRenderHercG) { _font.setCharWidth(16); } _fontLoaded = true; } void FreescapeEngine::loadMessagesFixedSize(Common::SeekableReadStream *file, int offset, int size, int number) { file->seek(offset); byte *buffer = (byte *)malloc(size + 1); buffer[size] = 0; debugC(1, kFreescapeDebugParser, "String table:"); for (int i = 0; i < number; i++) { file->read(buffer, size); Common::String message = (const char *)buffer; _messagesList.push_back(message); debugC(1, kFreescapeDebugParser, "'%s'", _messagesList[_messagesList.size() - 1].c_str()); } free(buffer); } void FreescapeEngine::loadDemoData(Common::SeekableReadStream *file, int offset, int size) { file->seek(offset); /*if (isAmiga()) { _demoData.push_back(0x50); _demoData.push_back(0x64); _demoData.push_back(0x30); _demoData.push_back(0x00); _demoData.push_back(0x64); _demoData.push_back(0x64); _demoData.push_back(0x5F); _demoData.push_back(0x00); }*/ debugC(1, kFreescapeDebugParser, "Reading demo data"); for (int i = 0; i < size; i++) { byte b = file->readByte(); _demoData.push_back(b); debugC(1, kFreescapeDebugParser, "%x", b); } } void FreescapeEngine::loadMessagesVariableSize(Common::SeekableReadStream *file, int offset, int number) { file->seek(offset); debugC(1, kFreescapeDebugParser, "String table:"); for (int i = 0; i < number; i++) { Common::String message = ""; while (true) { byte c = file->readByte(); if (c > 0xf0) c = ' '; if (c <= 1) break; message = message + c; } _messagesList.push_back(message); debugC(1, kFreescapeDebugParser, "'%s'", _messagesList[i].c_str()); } debugC(1, kFreescapeDebugParser, "End of messages at %" PRIx64, file->pos()); } void FreescapeEngine::loadGlobalObjects(Common::SeekableReadStream *file, int offset, int size) { debugC(1, kFreescapeDebugParser, "Loading global objects"); assert(!_areaMap.contains(255)); ObjectMap *globalObjectsByID = new ObjectMap; file->seek(offset); for (int i = 0; i < size; i++) { Object *gobj = load8bitObject(file); assert(gobj); assert(!globalObjectsByID->contains(gobj->getObjectID())); debugC(1, kFreescapeDebugParser, "Adding global object: %d", gobj->getObjectID()); (*globalObjectsByID)[gobj->getObjectID()] = gobj; } _areaMap[255] = new Area(255, 0, globalObjectsByID, nullptr, isCastle()); } void FreescapeEngine::parseAmigaAtariHeader(Common::SeekableReadStream *stream) { stream->seek(0x22); int size = stream->readUint16BE(); debugC(1, kFreescapeDebugParser, "Header table size %d", size); for (int i = 0; i < size; i++) { debugC(1, kFreescapeDebugParser, "Location: %x ", stream->readUint32BE()); Common::String filename; while (char c = stream->readByte()) filename += c; for (int j = filename.size() + 1; j < 16; j++) stream->readByte(); debugC(1, kFreescapeDebugParser, "Filename: %s", filename.c_str()); } } Common::SeekableReadStream *FreescapeEngine::decryptFileAmigaAtari(const Common::Path &packed, const Common::Path &unpacker, uint32 unpackArrayOffset) { Common::File file; file.open(packed); if (!file.isOpen()) error("Failed to open %s", packed.toString().c_str()); int size = file.size(); size -= size % 4; byte *encryptedBuffer = (byte *)malloc(size); file.read(encryptedBuffer, size); file.close(); uint32 d7 = 0; uint32 d6 = 0; byte *a6 = encryptedBuffer; byte *a5 = encryptedBuffer + size - 1; while (a6 <= a5) { uint64 d0 = (a6[0] << 24) | (a6[1] << 16) | (a6[2] << 8) | a6[3]; d0 = d0 + d6; d0 = uint32(d0); d0 = ((d0 << 3) & 0xFFFFFFFF) | ((d0 >> 29) & 0xFFFFFFFF); d0 ^= 0x71049763; d0 -= d7; d0 = ((d0 << 16) & 0xFFFF0000) | ((d0 >> 16) & 0xFFFF); a6[0] = byte((d0 >> 24) & 0xFF); //debug("%c", a6[0]); a6[1] = byte((d0 >> 16) & 0xFF); //debug("%c", a6[1]); a6[2] = byte((d0 >> 8) & 0xFF); //debug("%c", a6[2]); a6[3] = byte(d0 & 0xFF); //debug("%c", a6[3]); d6 += 5; d6 = ((d6 >> 3) & 0xFFFFFFFF) | ((d6 << 29) & 0xFFFFFFFF); d6 ^= 0x04000000; d7 += 4; a6 += 4; } file.open(unpacker); if (!file.isOpen()) error("Failed to open %s", unpacker.toString().c_str()); int originalSize = size; size = file.size(); byte *unpackArray = (byte *)malloc(size); file.read(unpackArray, size); file.close(); byte *unpackArrayPtr = unpackArray + unpackArrayOffset; uint32 i = 2 * 1024; do { uint8 ptr0 = unpackArrayPtr[2 * i]; //debug("%x -> %x", unpackArrayOffset + 2 * i, ptr0); uint8 ptr1 = unpackArrayPtr[2 * i + 1]; //debug("%x -> %x", unpackArrayOffset + 2 * i + 1, ptr1); uint8 val0 = unpackArrayPtr[2 * (i - 1)]; uint8 val1 = unpackArrayPtr[2 * (i - 1) + 1]; encryptedBuffer[2 * (ptr1 + 256 * ptr0)] = val0; encryptedBuffer[2 * (ptr1 + 256 * ptr0) + 1] = val1; i = i - 2; } while (i > 0); return (new Common::MemoryReadStream(encryptedBuffer, originalSize)); } } // namespace Freescape