803 lines
24 KiB
C++
803 lines
24 KiB
C++
/* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "alcachofa/alcachofa.h"
|
|
#include "alcachofa/rooms.h"
|
|
#include "alcachofa/script.h"
|
|
#include "alcachofa/global-ui.h"
|
|
#include "alcachofa/menu.h"
|
|
|
|
#include "common/file.h"
|
|
|
|
using namespace Common;
|
|
|
|
namespace Alcachofa {
|
|
|
|
Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {}
|
|
|
|
static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
|
|
if (type == ObjectBase::kClassName)
|
|
return new ObjectBase(room, stream);
|
|
else if (type == PointObject::kClassName)
|
|
return new PointObject(room, stream);
|
|
else if (type == GraphicObject::kClassName)
|
|
return new GraphicObject(room, stream);
|
|
else if (type == SpecialEffectObject::kClassName)
|
|
return new SpecialEffectObject(room, stream);
|
|
else if (type == Item::kClassName)
|
|
return new Item(room, stream);
|
|
else if (type == PhysicalObject::kClassName)
|
|
return new PhysicalObject(room, stream);
|
|
else if (type == MainMenuButton::kClassName)
|
|
return new MainMenuButton(room, stream);
|
|
else if (type == InternetMenuButton::kClassName)
|
|
return new InternetMenuButton(room, stream);
|
|
else if (type == OptionsMenuButton::kClassName)
|
|
return new OptionsMenuButton(room, stream);
|
|
else if (type == EditBox::kClassName)
|
|
return new EditBox(room, stream);
|
|
else if (type == PushButton::kClassName)
|
|
return new PushButton(room, stream);
|
|
else if (type == CheckBox::kClassName)
|
|
return new CheckBox(room, stream);
|
|
else if (type == CheckBoxAutoAdjustNoise::kClassName)
|
|
return new CheckBoxAutoAdjustNoise(room, stream);
|
|
else if (type == SlideButton::kClassName)
|
|
return new SlideButton(room, stream);
|
|
else if (type == IRCWindow::kClassName)
|
|
return new IRCWindow(room, stream);
|
|
else if (type == MessageBox::kClassName)
|
|
return new MessageBox(room, stream);
|
|
else if (type == VoiceMeter::kClassName)
|
|
return new VoiceMeter(room, stream);
|
|
else if (type == InteractableObject::kClassName)
|
|
return new InteractableObject(room, stream);
|
|
else if (type == Door::kClassName)
|
|
return new Door(room, stream);
|
|
else if (type == Character::kClassName)
|
|
return new Character(room, stream);
|
|
else if (type == WalkingCharacter::kClassName)
|
|
return new WalkingCharacter(room, stream);
|
|
else if (type == MainCharacter::kClassName)
|
|
return new MainCharacter(room, stream);
|
|
else if (type == FloorColor::kClassName)
|
|
return new FloorColor(room, stream);
|
|
else
|
|
return nullptr; // handled in Room::Room
|
|
}
|
|
|
|
Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
|
|
: _world(world) {
|
|
_name = readVarString(stream);
|
|
_musicId = (int)stream.readByte();
|
|
_characterAlphaTint = stream.readByte();
|
|
auto backgroundScale = stream.readSint16LE();
|
|
_floors[0] = PathFindingShape(stream);
|
|
_floors[1] = PathFindingShape(stream);
|
|
_fixedCameraOnEntering = readBool(stream);
|
|
PathFindingShape _(stream); // unused path finding area
|
|
_characterAlphaPremultiplier = stream.readByte();
|
|
if (hasUselessByte)
|
|
stream.readByte();
|
|
|
|
uint32 objectEnd = stream.readUint32LE();
|
|
while (objectEnd > 0) {
|
|
const auto type = readVarString(stream);
|
|
auto object = readRoomObject(this, type, stream);
|
|
if (object == nullptr) {
|
|
g_engine->game().unknownRoomObject(type);
|
|
stream.seek(objectEnd, SEEK_SET);
|
|
} else if (stream.pos() < objectEnd) {
|
|
g_engine->game().notEnoughObjectDataRead(_name.c_str(), stream.pos(), objectEnd);
|
|
stream.seek(objectEnd, SEEK_SET);
|
|
} else if (stream.pos() > objectEnd) // this is probably not recoverable
|
|
error("Read past the object data (%u > %lld) in room %s", objectEnd, (long long int)stream.pos(), _name.c_str());
|
|
|
|
if (object != nullptr)
|
|
_objects.push_back(object);
|
|
objectEnd = stream.readUint32LE();
|
|
}
|
|
if (g_engine->game().doesRoomHaveBackground(this))
|
|
_objects.push_back(new Background(this, _name, backgroundScale));
|
|
|
|
if (!_floors[0].empty())
|
|
_activeFloorI = 0;
|
|
}
|
|
|
|
Room::~Room() {
|
|
for (auto *object : _objects)
|
|
delete object;
|
|
}
|
|
|
|
ObjectBase *Room::getObjectByName(const char *name) const {
|
|
for (auto *object : _objects) {
|
|
if (object->name().equalsIgnoreCase(name))
|
|
return object;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Room::update() {
|
|
if (g_engine->isDebugModeActive())
|
|
return;
|
|
updateScripts();
|
|
|
|
if (g_engine->player().currentRoom() == this) {
|
|
updateRoomBounds();
|
|
g_engine->globalUI().updateClosingInventory();
|
|
if (!updateInput())
|
|
return;
|
|
}
|
|
if (!g_engine->menu().isOpen() &&
|
|
g_engine->player().currentRoom() != &g_engine->world().inventory())
|
|
world().globalRoom().updateObjects();
|
|
if (g_engine->player().currentRoom() == this)
|
|
updateObjects();
|
|
}
|
|
|
|
void Room::draw() {
|
|
g_engine->camera().update();
|
|
drawObjects();
|
|
world().globalRoom().drawObjects();
|
|
g_engine->globalUI().drawScreenStates();
|
|
g_engine->drawQueue().draw();
|
|
drawDebug();
|
|
world().globalRoom().drawDebug();
|
|
}
|
|
|
|
void Room::updateScripts() {
|
|
g_engine->game().updateScriptVariables();
|
|
if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
|
|
g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, ScriptFlags::AllowMissing | ScriptFlags::IsBackground);
|
|
g_engine->scheduler().run();
|
|
}
|
|
|
|
bool Room::updateInput() {
|
|
auto &player = g_engine->player();
|
|
auto &input = g_engine->input();
|
|
if (player.heldItem() != nullptr && !player.activeCharacter()->isBusy() && input.wasMouseRightPressed()) {
|
|
player.heldItem() = nullptr;
|
|
return false;
|
|
}
|
|
|
|
bool canInteract = !player.activeCharacter()->isBusy();
|
|
// A complicated network condition can prevent interaction at this point
|
|
if (g_engine->menu().isOpen() || !player.isGameLoaded())
|
|
canInteract = true;
|
|
if (canInteract) {
|
|
player.resetCursor();
|
|
if (!g_engine->globalUI().updateChangingCharacter() &&
|
|
!g_engine->globalUI().updateOpeningInventory()) {
|
|
updateInteraction();
|
|
player.updateCursor();
|
|
}
|
|
player.drawCursor();
|
|
}
|
|
|
|
if (player.currentRoom() == this) {
|
|
g_engine->globalUI().drawChangingButton();
|
|
g_engine->menu().updateOpeningMenu();
|
|
}
|
|
|
|
return player.currentRoom() == this;
|
|
}
|
|
|
|
void Room::updateInteraction() {
|
|
auto &player = g_engine->player();
|
|
auto &input = g_engine->input();
|
|
|
|
player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
|
|
if (player.selectedObject() == nullptr) {
|
|
if (input.wasMouseLeftPressed() && _activeFloorI >= 0 &&
|
|
player.activeCharacter()->room() == this &&
|
|
player.pressedObject() == nullptr) {
|
|
player.activeCharacter()->walkToMouse();
|
|
g_engine->camera().setFollow(player.activeCharacter());
|
|
}
|
|
} else {
|
|
player.selectedObject()->markSelected();
|
|
if (input.wasAnyMousePressed())
|
|
player.pressedObject() = player.selectedObject();
|
|
}
|
|
}
|
|
|
|
void Room::updateRoomBounds() {
|
|
auto background = getObjectByName("Background");
|
|
auto graphic = background == nullptr ? nullptr : background->graphic();
|
|
if (graphic != nullptr) {
|
|
auto bgSize = graphic->animation().imageSize(0);
|
|
/* This fixes a bug where if the background image is invalid the original engine
|
|
* would not update the background size. This would be around 1024,768 due to
|
|
* previous rooms in the bug instances I found.
|
|
*/
|
|
if (bgSize == Point(0, 0))
|
|
bgSize = Point(1024, 768);
|
|
g_engine->camera().setRoomBounds(bgSize, graphic->scale());
|
|
}
|
|
}
|
|
|
|
void Room::updateObjects() {
|
|
const auto *previousRoom = g_engine->player().currentRoom();
|
|
for (auto *object : _objects) {
|
|
object->update();
|
|
if (g_engine->player().currentRoom() != previousRoom)
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Room::drawObjects() {
|
|
for (auto *object : _objects) {
|
|
object->draw();
|
|
}
|
|
}
|
|
|
|
void Room::drawDebug() {
|
|
auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
|
|
if (renderer == nullptr || !g_engine->console().isAnyDebugDrawingOn())
|
|
return;
|
|
for (auto *object : _objects) {
|
|
if (object->room() == g_engine->player().currentRoom())
|
|
object->drawDebug();
|
|
}
|
|
if (_activeFloorI < 0)
|
|
return;
|
|
auto &floor = _floors[_activeFloorI];
|
|
if (g_engine->console().showFloor())
|
|
renderer->debugShape(floor, kDebugBlue);
|
|
|
|
if (g_engine->console().showFloorEdges()) {
|
|
auto &camera = g_engine->camera();
|
|
for (uint polygonI = 0; polygonI < floor.polygonCount(); polygonI++) {
|
|
auto polygon = floor.at(polygonI);
|
|
for (uint pointI = 0; pointI < polygon._points.size(); pointI++) {
|
|
int32 targetI = floor.edgeTarget(polygonI, pointI);
|
|
if (targetI < 0)
|
|
continue;
|
|
Point a = camera.transform3Dto2D(polygon._points[pointI]);
|
|
Point b = camera.transform3Dto2D(polygon._points[(pointI + 1) % polygon._points.size()]);
|
|
Point source = (a + b) / 2;
|
|
Point target = camera.transform3Dto2D(floor.at((uint)targetI).midPoint());
|
|
renderer->debugPolyline(source, target, kDebugLightBlue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Room::loadResources() {
|
|
for (auto *object : _objects)
|
|
object->loadResources();
|
|
|
|
// this fixes some camera backups not working when closing the inventory
|
|
if (g_engine->player().currentRoom() == this)
|
|
updateRoomBounds();
|
|
}
|
|
|
|
void Room::freeResources() {
|
|
for (auto *object : _objects)
|
|
object->freeResources();
|
|
}
|
|
|
|
void Room::syncGame(Serializer &serializer) {
|
|
serializer.syncAsSByte(_activeFloorI);
|
|
for (auto *object : _objects)
|
|
object->syncGame(serializer);
|
|
}
|
|
|
|
void Room::toggleActiveFloor() {
|
|
_activeFloorI ^= 1;
|
|
}
|
|
|
|
ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
|
|
for (auto object : _objects) {
|
|
auto shape = object->shape();
|
|
auto shapeObject = dynamic_cast<ShapeObject *>(object);
|
|
if (!object->isEnabled() ||
|
|
shape == nullptr || shapeObject == nullptr ||
|
|
object->room() != g_engine->player().currentRoom() || // e.g. a main character that is in another room
|
|
!shape->contains(g_engine->input().mousePos3D()))
|
|
continue;
|
|
if (best == nullptr || shapeObject->order() < best->order())
|
|
best = shapeObject;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
|
|
: Room(world, stream, true) {}
|
|
|
|
bool OptionsMenu::updateInput() {
|
|
if (!Room::updateInput())
|
|
return false;
|
|
|
|
auto currentSelectedObject = g_engine->player().selectedObject();
|
|
if (currentSelectedObject == nullptr) {
|
|
if (_lastSelectedObject == nullptr) {
|
|
if (_idleArm != nullptr)
|
|
_idleArm->toggle(true);
|
|
return true;
|
|
}
|
|
|
|
_lastSelectedObject->markSelected();
|
|
} else
|
|
_lastSelectedObject = currentSelectedObject;
|
|
if (_idleArm != nullptr)
|
|
_idleArm->toggle(false);
|
|
return true;
|
|
}
|
|
|
|
void OptionsMenu::loadResources() {
|
|
Room::loadResources();
|
|
_lastSelectedObject = nullptr;
|
|
_currentSlideButton = nullptr;
|
|
_idleArm = getObjectByName("Brazo");
|
|
}
|
|
|
|
void OptionsMenu::clearLastSelectedObject() {
|
|
_lastSelectedObject = nullptr;
|
|
}
|
|
|
|
ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
|
|
: Room(world, stream, true) {}
|
|
|
|
ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
|
|
: Room(world, stream, true) {}
|
|
|
|
Inventory::Inventory(World *world, SeekableReadStream &stream)
|
|
: Room(world, stream, true) {}
|
|
|
|
Inventory::~Inventory() {
|
|
// No need to delete items, they are room objects and thus deleted in Room::~Room
|
|
}
|
|
|
|
bool Inventory::updateInput() {
|
|
auto &player = g_engine->player();
|
|
auto &input = g_engine->input();
|
|
auto *hoveredItem = getHoveredItem();
|
|
|
|
if (!player.activeCharacter()->isBusy())
|
|
player.drawCursor(0);
|
|
|
|
if (hoveredItem != nullptr && !player.activeCharacter()->isBusy()) {
|
|
if ((input.wasMouseLeftPressed() && player.heldItem() == nullptr) ||
|
|
(input.wasMouseLeftReleased() && player.heldItem() != nullptr) ||
|
|
input.wasMouseRightReleased()) {
|
|
hoveredItem->trigger();
|
|
player.pressedObject() = nullptr;
|
|
}
|
|
|
|
g_engine->drawQueue().add<TextDrawRequest>(
|
|
g_engine->globalUI().generalFont(),
|
|
g_engine->world().getLocalizedName(hoveredItem->name()),
|
|
input.mousePos2D() + Point(0, -50),
|
|
-1, true, kWhite, -kForegroundOrderCount + 1);
|
|
}
|
|
|
|
const bool userWantsToCloseInventory =
|
|
closeInventoryTriggerBounds().contains(input.mousePos2D()) ||
|
|
input.wasMenuKeyPressed() ||
|
|
input.wasInventoryKeyPressed();
|
|
if (!player.activeCharacter()->isBusy() &&
|
|
userWantsToCloseInventory) {
|
|
player.changeRoomToBeforeInventory();
|
|
close();
|
|
}
|
|
|
|
if (!player.activeCharacter()->isBusy() &&
|
|
hoveredItem == nullptr &&
|
|
input.wasMouseRightReleased()) {
|
|
player.heldItem() = nullptr;
|
|
return false;
|
|
}
|
|
|
|
return player.currentRoom() == this;
|
|
}
|
|
|
|
Item *Inventory::getHoveredItem() {
|
|
auto mousePos = g_engine->input().mousePos2D();
|
|
for (auto item : _items) {
|
|
if (!item->isEnabled())
|
|
continue;
|
|
if (g_engine->player().heldItem() != nullptr &&
|
|
g_engine->player().heldItem()->name().equalsIgnoreCase(item->name()))
|
|
continue;
|
|
|
|
auto graphic = item->graphic();
|
|
assert(graphic != nullptr);
|
|
auto bounds = graphic->animation().frameBounds(0);
|
|
auto totalOffset = graphic->animation().totalFrameOffset(0);
|
|
auto delta = mousePos - graphic->topLeft() - totalOffset;
|
|
if (delta.x >= 0 && delta.y >= 0 && delta.x <= bounds.width() && delta.y <= bounds.height())
|
|
return item;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Inventory::initItems() {
|
|
auto &mortadelo = world().mortadelo();
|
|
auto &filemon = world().filemon();
|
|
for (auto object : _objects) {
|
|
auto item = dynamic_cast<Item *>(object);
|
|
if (item == nullptr)
|
|
continue;
|
|
_items.push_back(item);
|
|
mortadelo._items.push_back(new Item(*item));
|
|
filemon._items.push_back(new Item(*item));
|
|
}
|
|
}
|
|
|
|
void Inventory::updateItemsByActiveCharacter() {
|
|
auto *character = g_engine->player().activeCharacter();
|
|
assert(character != nullptr);
|
|
for (auto *item : _items)
|
|
item->toggle(character->hasItem(item->name()));
|
|
}
|
|
|
|
void Inventory::drawAsOverlay(int32 scrollY) {
|
|
for (auto object : _objects) {
|
|
auto graphic = object->graphic();
|
|
if (graphic == nullptr)
|
|
continue;
|
|
|
|
int16 oldY = graphic->topLeft().y;
|
|
int8 oldOrder = graphic->order();
|
|
graphic->topLeft().y += scrollY;
|
|
graphic->order() = -kForegroundOrderCount;
|
|
if (object->name().equalsIgnoreCase("Background"))
|
|
graphic->order()++;
|
|
object->draw();
|
|
graphic->topLeft().y = oldY;
|
|
graphic->order() = oldOrder;
|
|
}
|
|
}
|
|
|
|
void Inventory::open() {
|
|
g_engine->camera().backup(1);
|
|
g_engine->player().changeRoom(name(), true);
|
|
updateItemsByActiveCharacter();
|
|
}
|
|
|
|
void Inventory::close() {
|
|
g_engine->camera().restore(1);
|
|
g_engine->globalUI().startClosingInventory();
|
|
}
|
|
|
|
void Room::debugPrint(bool withObjects) const {
|
|
auto &console = g_engine->console();
|
|
console.debugPrintf(" %s\n", _name.c_str());
|
|
if (!withObjects)
|
|
return;
|
|
|
|
for (auto *object : _objects) {
|
|
console.debugPrintf("\t%20s %-32s %s\n",
|
|
object->typeName(),
|
|
object->name().c_str(),
|
|
object->isEnabled() ? "" : "disabled");
|
|
}
|
|
}
|
|
|
|
static constexpr const char *kMapFiles[] = {
|
|
"MAPAS/MAPA5.EMC",
|
|
"MAPAS/MAPA4.EMC",
|
|
"MAPAS/MAPA3.EMC",
|
|
"MAPAS/MAPA2.EMC",
|
|
"MAPAS/MAPA1.EMC",
|
|
"MAPAS/GLOBAL.EMC",
|
|
nullptr
|
|
};
|
|
|
|
World::World() {
|
|
for (auto *itMapFile = kMapFiles; *itMapFile != nullptr; itMapFile++) {
|
|
if (loadWorldFile(*itMapFile))
|
|
_loadedMapCount++;
|
|
}
|
|
loadLocalizedNames();
|
|
loadDialogLines();
|
|
|
|
_globalRoom = getRoomByName("GLOBAL");
|
|
if (_globalRoom == nullptr)
|
|
error("Could not find GLOBAL room");
|
|
_inventory = dynamic_cast<Inventory *>(getRoomByName("INVENTARIO"));
|
|
if (_inventory == nullptr)
|
|
error("Could not find INVENTARIO");
|
|
_filemon = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("FILEMON"));
|
|
if (_filemon == nullptr)
|
|
error("Could not find FILEMON");
|
|
_mortadelo = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("MORTADELO"));
|
|
if (_mortadelo == nullptr)
|
|
error("Could not find MORTADELO");
|
|
|
|
_inventory->initItems();
|
|
}
|
|
|
|
World::~World() {
|
|
for (auto *room : _rooms)
|
|
delete room;
|
|
}
|
|
|
|
MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
|
|
switch (kind) {
|
|
case MainCharacterKind::Mortadelo:
|
|
return *_mortadelo;
|
|
case MainCharacterKind::Filemon:
|
|
return *_filemon;
|
|
default:
|
|
error("Invalid character kind given to getMainCharacterByKind");
|
|
}
|
|
}
|
|
|
|
MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const {
|
|
switch (kind) {
|
|
case MainCharacterKind::Mortadelo:
|
|
return *_filemon;
|
|
case MainCharacterKind::Filemon:
|
|
return *_mortadelo;
|
|
default:
|
|
error("Invalid character kind given to getOtherMainCharacterByKind");
|
|
}
|
|
}
|
|
|
|
Room *World::getRoomByName(const char *name) const {
|
|
assert(name != nullptr);
|
|
if (*name == '\0')
|
|
return nullptr;
|
|
for (auto *room : _rooms) {
|
|
if (room->name().equalsIgnoreCase(name))
|
|
return room;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
ObjectBase *World::getObjectByName(const char *name) const {
|
|
ObjectBase *result = nullptr;
|
|
if (g_engine->player().currentRoom() != nullptr)
|
|
result = g_engine->player().currentRoom()->getObjectByName(name);
|
|
if (result == nullptr)
|
|
result = globalRoom().getObjectByName(name);
|
|
if (result == nullptr)
|
|
result = inventory().getObjectByName(name);
|
|
return result;
|
|
}
|
|
|
|
ObjectBase *World::getObjectByName(MainCharacterKind character, const char *name) const {
|
|
if (character == MainCharacterKind::None)
|
|
return getObjectByName(name);
|
|
const auto &player = g_engine->player();
|
|
ObjectBase *result = nullptr;
|
|
if (player.activeCharacterKind() == character && player.currentRoom() != player.activeCharacter()->room())
|
|
result = player.currentRoom()->getObjectByName(name);
|
|
if (result == nullptr)
|
|
result = player.activeCharacter()->room()->getObjectByName(name);
|
|
if (result == nullptr)
|
|
result = globalRoom().getObjectByName(name);
|
|
if (result == nullptr)
|
|
result = inventory().getObjectByName(name);
|
|
return result;
|
|
}
|
|
|
|
ObjectBase *World::getObjectByNameFromAnyRoom(const char *name) const {
|
|
assert(name != nullptr);
|
|
if (*name == '\0')
|
|
return nullptr;
|
|
for (auto *room : _rooms) {
|
|
ObjectBase *result = room->getObjectByName(name);
|
|
if (result != nullptr)
|
|
return result;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void World::toggleObject(MainCharacterKind character, const char *objName, bool isEnabled) {
|
|
ObjectBase *object = getObjectByName(character, objName);
|
|
if (object == nullptr)
|
|
object = getObjectByNameFromAnyRoom(objName);
|
|
if (object == nullptr) // I would have liked an error for this, but original inconsistencies...
|
|
warning("Tried to toggle unknown object: %s", objName);
|
|
else
|
|
object->toggle(isEnabled);
|
|
}
|
|
|
|
const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
|
|
int kindI = (int)kind;
|
|
assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
|
|
return _globalAnimationNames[kindI];
|
|
}
|
|
|
|
const char *World::getLocalizedName(const String &name) const {
|
|
const char *localizedName;
|
|
return _localizedNames.tryGetVal(name.c_str(), localizedName)
|
|
? localizedName
|
|
: name.c_str();
|
|
}
|
|
|
|
const char *World::getDialogLine(int32 dialogId) const {
|
|
if (dialogId < 0 || (uint)dialogId >= _dialogLines.size())
|
|
error("Invalid dialog line index %d", dialogId);
|
|
return _dialogLines[dialogId];
|
|
}
|
|
|
|
static Room *readRoom(World *world, SeekableReadStream &stream) {
|
|
const auto type = readVarString(stream);
|
|
if (type == Room::kClassName)
|
|
return new Room(world, stream);
|
|
else if (type == OptionsMenu::kClassName)
|
|
return new OptionsMenu(world, stream);
|
|
else if (type == ConnectMenu::kClassName)
|
|
return new ConnectMenu(world, stream);
|
|
else if (type == ListenMenu::kClassName)
|
|
return new ListenMenu(world, stream);
|
|
else if (type == Inventory::kClassName)
|
|
return new Inventory(world, stream);
|
|
else {
|
|
g_engine->game().unknownRoomType(type);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool World::loadWorldFile(const char *path) {
|
|
Common::File file;
|
|
if (!file.open(path)) {
|
|
// this is not necessarily an error, apparently the demos just have less
|
|
// chapter files. Being a demo is then also stored in some script vars
|
|
warning("Could not open world file %s\n", path);
|
|
return false;
|
|
}
|
|
|
|
// the first chunk seems to be debug symbols and/or info about the file structure
|
|
// it is ignored in the published game.
|
|
auto startOffset = file.readUint32LE();
|
|
file.seek(startOffset, SEEK_SET);
|
|
skipVarString(file); // some more unused strings related to development files?
|
|
skipVarString(file);
|
|
skipVarString(file);
|
|
skipVarString(file);
|
|
skipVarString(file);
|
|
skipVarString(file);
|
|
|
|
_initScriptName = readVarString(file);
|
|
skipVarString(file); // would be _updateScriptName, but it is never called
|
|
for (int i = 0; i < (int)GlobalAnimationKind::Count; i++)
|
|
_globalAnimationNames[i] = readVarString(file);
|
|
|
|
uint32 roomEnd = file.readUint32LE();
|
|
while (roomEnd > 0) {
|
|
Room *room = readRoom(this, file);
|
|
if (room != nullptr)
|
|
_rooms.push_back(room);
|
|
if (file.pos() < roomEnd) {
|
|
g_engine->game().notEnoughRoomDataRead(path, file.pos(), roomEnd);
|
|
file.seek(roomEnd, SEEK_SET);
|
|
} else if (file.pos() > roomEnd) // this surely is not recoverable
|
|
error("Read past the room data for world %s", path);
|
|
roomEnd = file.readUint32LE();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Behold the incredible encryption of text files:
|
|
* - first 32 bytes are cipher
|
|
* - next byte is the XOR key
|
|
* - next 4 bytes are garbage
|
|
* - rest of the file is cipher
|
|
*/
|
|
static void loadEncryptedFile(const char *path, Array<char> &output) {
|
|
constexpr uint kHeaderSize = 32;
|
|
File file;
|
|
if (!file.open(path))
|
|
error("Could not open text file %s", path);
|
|
output.resize(file.size() - 4 - 1 + 1); // garbage bytes, key and we add a zero terminator for safety
|
|
if (file.read(output.data(), kHeaderSize) != kHeaderSize)
|
|
error("Could not read text file header");
|
|
char key = file.readSByte();
|
|
uint remainingSize = output.size() - kHeaderSize - 1;
|
|
if (!file.skip(4) || file.read(output.data() + kHeaderSize, remainingSize) != remainingSize)
|
|
error("Could not read text file body");
|
|
for (auto &ch : output)
|
|
ch ^= key;
|
|
output.back() = '\0'; // one for good measure and a zero-terminator
|
|
}
|
|
|
|
static char *trimLeading(char *start, char *end) {
|
|
while (start < end && isSpace(*start))
|
|
start++;
|
|
return start;
|
|
}
|
|
|
|
static char *skipWord(char *start, char *end) {
|
|
while (start < end && !isSpace(*start))
|
|
start++;
|
|
return start;
|
|
}
|
|
|
|
static char *trimTrailing(char *start, char *end) {
|
|
while (start < end && isSpace(end[-1]))
|
|
end--;
|
|
return end;
|
|
}
|
|
|
|
void World::loadLocalizedNames() {
|
|
loadEncryptedFile("Textos/OBJETOS.nkr", _namesChunk);
|
|
char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end() - 1;
|
|
while (lineStart < fileEnd) {
|
|
char *lineEnd = find(lineStart, fileEnd, '\n');
|
|
char *keyEnd = find(lineStart, lineEnd, '#');
|
|
if (keyEnd == lineStart || keyEnd == lineEnd || keyEnd + 1 == lineEnd)
|
|
error("Invalid localized name line separator");
|
|
char *valueEnd = trimTrailing(keyEnd + 1, lineEnd);
|
|
|
|
*keyEnd = 0;
|
|
*valueEnd = 0;
|
|
if (valueEnd == keyEnd + 1) {
|
|
// happens in the english version of Movie Adventure
|
|
warning("Empty localized name for %s", lineStart);
|
|
}
|
|
|
|
_localizedNames[lineStart] = keyEnd + 1;
|
|
lineStart = lineEnd + 1;
|
|
}
|
|
}
|
|
|
|
void World::loadDialogLines() {
|
|
/* This "encrypted" file contains lines in any of the following formats:
|
|
* Name 123, "This is the dialog line"\r\n
|
|
* Name 123, "This is the dialog line\r\n
|
|
* Name 123 This is the dialog line \r\n
|
|
*
|
|
* - The ID does not have to be correct, it is ignored by the original engine.
|
|
* - We only need the dialog line and insert null-terminators where appropriate.
|
|
*/
|
|
loadEncryptedFile("Textos/DIALOGOS.nkr", _dialogChunk);
|
|
char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end() - 1;
|
|
while (lineStart < fileEnd) {
|
|
char *lineEnd = find(lineStart, fileEnd, '\n');
|
|
|
|
char *cursor = trimLeading(lineStart, lineEnd); // space before the name
|
|
cursor = skipWord(cursor, lineEnd); // the name
|
|
cursor = trimLeading(cursor, lineEnd); // space between dialog id
|
|
cursor = skipWord(cursor, lineEnd); // the dialog id
|
|
cursor = trimLeading(cursor, lineEnd); // space between id and line
|
|
char *dialogLineEnd = trimTrailing(cursor, lineEnd);
|
|
if (*cursor == '\"')
|
|
cursor++;
|
|
if (dialogLineEnd > cursor && dialogLineEnd[-1] == '\"')
|
|
dialogLineEnd--;
|
|
|
|
if (cursor >= dialogLineEnd) {
|
|
if (cursor > dialogLineEnd)
|
|
g_engine->game().invalidDialogLine(_dialogLines.size());
|
|
cursor = lineStart; // store an empty string
|
|
dialogLineEnd = lineStart;
|
|
}
|
|
|
|
*dialogLineEnd = 0;
|
|
_dialogLines.push_back(cursor);
|
|
lineStart = lineEnd + 1;
|
|
}
|
|
}
|
|
|
|
void World::syncGame(Serializer &s) {
|
|
for (Room *room : _rooms)
|
|
room->syncGame(s);
|
|
}
|
|
|
|
}
|