Files
2026-02-02 04:50:13 +01:00

614 lines
17 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 "ultima/ultima8/misc/debugger.h"
#include "ultima/ultima8/world/world.h"
#include "ultima/ultima8/world/map.h"
#include "ultima/ultima8/world/current_map.h"
#include "ultima/ultima8/filesys/flex_file.h"
#include "ultima/ultima8/filesys/raw_archive.h"
#include "ultima/ultima8/world/item_factory.h"
#include "ultima/ultima8/world/actors/main_actor.h"
#include "ultima/ultima8/world/actors/scheduler_process.h"
#include "ultima/ultima8/world/loop_script.h"
#include "ultima/ultima8/usecode/uc_list.h"
#include "ultima/ultima8/misc/direction_util.h"
#include "ultima/ultima8/games/game_data.h"
#include "ultima/ultima8/kernel/kernel.h"
#include "ultima/ultima8/kernel/object_manager.h"
#include "ultima/ultima8/world/camera_process.h" // for resetting the camera
#include "ultima/ultima8/gumps/gump.h" // For CloseItemDependents notification
#include "ultima/ultima8/world/get_object.h"
#include "ultima/ultima8/world/target_reticle_process.h"
#include "ultima/ultima8/audio/audio_process.h"
#include "ultima/ultima8/world/snap_process.h"
#include "ultima/ultima8/gfx/main_shape_archive.h"
namespace Ultima {
namespace Ultima8 {
//#define DUMP_ITEMS
World *World::_world = nullptr;
World::World() : _currentMap(nullptr), _alertActive(false), _difficulty(3),
_controlledNPCNum(1), _vargasShield(5000) {
debug(1, "Creating World...");
_world = this;
}
World::~World() {
debug(1, "Destroying World...");
clear();
_world = nullptr;
}
void World::clear() {
unsigned int i;
for (i = 0; i < _maps.size(); ++i) {
delete _maps[i];
}
_maps.clear();
_ethereal.clear();
if (_currentMap)
delete _currentMap;
_currentMap = nullptr;
_alertActive = false;
_controlledNPCNum = 1;
_vargasShield = 5000;
}
void World::reset() {
debug(1, "Resetting World...");
clear();
initMaps();
}
void World::initMaps() {
// Q: How do we determine which Maps to create? Only create those
// with non-zero size in fixed.dat?
_maps.resize(256);
for (unsigned int i = 0; i < 256; ++i) {
_maps[i] = new Map(i);
}
_currentMap = new CurrentMap();
}
bool World::switchMap(uint32 newmap) {
assert(_currentMap);
if (_currentMap->getNum() == newmap)
return true;
if (newmap >= _maps.size() || _maps[newmap] == nullptr)
return false; // no such map
// Map switching procedure:
// stop all sound effects (except speech, such as Guardian barks)
// notify all gumps of a map change
// delete any ethereal objects
// write back CurrentMap to the old map, which
// deletes all disposable items
// deletes the EggHatcher
// resets all eggs
// swap out fixed items in old map
// kill all processes (except those of type 1 or of item 0)
// load fixed items in new map
// load new map into CurrentMap, which also
// assigns objIDs to fixed items
// assigns objIDs to nonfixed items
// creates an EggHatcher and notifies it of all eggs
// sets up all NPCs in the new map
// update camera if needed
AudioProcess *ap = AudioProcess::get_instance();
if (ap) ap->stopAllExceptSpeech();
// Notify all the gumps of the mapchange
Ultima8Engine *gui = Ultima8Engine::get_instance();
if (gui) {
Gump *desktop = gui->getDesktopGump();
if (desktop) desktop->CloseItemDependents();
}
// get rid of any remaining ethereal items
while (!_ethereal.empty()) {
uint16 eth = _ethereal.front();
_ethereal.pop_front();
Item *i = getItem(eth);
if (i) {
if (i->getFlags() & Item::FLG_ETHEREAL)
i->destroy();
else
warning("Not destroying ethereal item %d - it doesn't think it's ethereal", eth);
}
}
uint32 oldmap = _currentMap->getNum();
if (oldmap != 0) {
debug(1, "Unloading map %u", oldmap);
assert(oldmap < _maps.size() && _maps[oldmap] != nullptr);
_currentMap->writeback();
debug(1, "Unloading Fixed items from map %u", oldmap);
_maps[oldmap]->unloadFixed();
}
// Kill any processes that need killing
if (GAME_IS_U8) {
// U8 doesn't kill processes of object 0 *or* type 1 when changing map.
Kernel::get_instance()->killProcessesNotOfType(0, 1, true);
} else {
// Crusader kills processes even for object 0 when switching.
SnapProcess::get_instance()->clearEggs();
CameraProcess::ResetCameraProcess();
Kernel::get_instance()->killAllProcessesNotOfTypeExcludeCurrent(1, true);
Kernel::get_instance()->addProcess(new SchedulerProcess());
}
debug(1, "Loading Fixed items in map %u", newmap);
Common::SeekableReadStream *items = GameData::get_instance()->getFixed()
->get_datasource(newmap);
_maps[newmap]->loadFixed(items);
delete items;
_currentMap->loadMap(_maps[newmap]);
// Update camera
if (GAME_IS_U8) {
// TODO: This may not even be needed for U8, but reset in case camera
// was looking at something other than the avatar during teleport.
CameraProcess *camera = CameraProcess::GetCameraProcess();
if (camera && camera->getItemNum() != kMainActorId) {
CameraProcess::SetCameraProcess(new CameraProcess(kMainActorId));
}
CameraProcess::SetEarthquake(0);
} else {
// In Crusader, snap the camera to the avatar. The snap process will
// then find the right snap egg in the next frame.
CameraProcess::SetCameraProcess(new CameraProcess(kMainActorId));
}
return true;
}
void World::loadNonFixed(Common::SeekableReadStream *rs) {
FlexFile *f = new FlexFile(rs);
debug(1, "Loading NonFixed items");
for (unsigned int i = 0; i < f->getCount(); ++i) {
// items in this map?
if (f->getSize(i) > 0) {
assert(_maps.size() > i);
assert(_maps[i] != nullptr);
Common::SeekableReadStream *items = f->getDataSource(i);
_maps[i]->loadNonFixed(items);
delete items;
}
}
delete f;
}
void World::loadItemCachNPCData(Common::SeekableReadStream *itemcach, Common::SeekableReadStream *npcdata) {
FlexFile *itemcachflex = new FlexFile(itemcach);
FlexFile *npcdataflex = new FlexFile(npcdata);
Common::SeekableReadStream *itemds = itemcachflex->getDataSource(0);
Common::SeekableReadStream *npcds = npcdataflex->getDataSource(0);
delete itemcachflex;
delete npcdataflex;
debug(1, "Loading NPCs");
for (uint32 i = 1; i < 256; ++i) { // Get rid of constants?
// These are ALL unsigned on disk
itemds->seek(0x00000 + i * 2);
int32 x = static_cast<int32>(itemds->readUint16LE());
itemds->seek(0x04800 + i * 2);
int32 y = static_cast<int32>(itemds->readUint16LE());
itemds->seek(0x09000 + i * 1);
int32 z = static_cast<int32>(itemds->readByte());
itemds->seek(0x0B400 + i * 2);
uint32 shape = itemds->readUint16LE();
itemds->seek(0x0FC00 + i * 1);
uint32 frame = itemds->readByte();
itemds->seek(0x12000 + i * 2);
uint16 flags = itemds->readUint16LE();
itemds->seek(0x16800 + i * 2);
uint16 quality = itemds->readUint16LE();
itemds->seek(0x1B000 + i * 1);
uint16 npcnum = static_cast<uint8>(itemds->readByte());
itemds->seek(0x1D400 + i * 1);
uint16 mapnum = static_cast<uint8>(itemds->readByte());
itemds->seek(0x1F800 + i * 2);
//uint16 next;
(void)itemds->readUint16LE();
// half the frame number is stored in npcdata.dat
npcds->seek(7 + i * 0x31);
frame += npcds->readByte() << 8;
if (shape == 0) {
// U8's itemcach has a lot of garbage in it.
// Ignore it.
continue;
}
#ifdef DUMP_ITEMS
debugC(kDebugObject, "%u,%u:\t(%d, %d, %d),\t%04X, %u, %u, u",
shape, frame, x, y, z, flags, quality, npcnum, mapnum);
#endif
Actor *actor = ItemFactory::createActor(shape, frame, quality,
flags | Item::FLG_IN_NPC_LIST,
npcnum, mapnum,
Item::EXT_PERMANENT_NPC, false);
if (!actor) {
warning("Couldn't create actor");
continue;
}
ObjectManager::get_instance()->assignActorObjId(actor, i);
actor->setLocation(x, y, z);
// read npcdata:
npcds->seek(i * 0x31);
actor->setStr(npcds->readByte()); // 0x00: strength
actor->setDex(npcds->readByte()); // 0x01: dexterity
actor->setInt(npcds->readByte()); // 0x02: intelligence
actor->setHP(npcds->readByte()); // 0x03: hitpoints
actor->setDir(Direction_FromUsecodeDir(npcds->readByte())); // 0x04: direction
uint16 la = npcds->readUint16LE(); // 0x05,0x06: last anim
actor->setLastAnim(static_cast<Animation::Sequence>(la));
npcds->skip(1); // 0x07: high byte of framenum
npcds->skip(1); // 0x08: current anim frame
npcds->skip(1); // 0x09: start Z of current fall
npcds->skip(1); // 0x0A: unknown, always zero
uint8 align = npcds->readByte(); // 0x0B: alignments
actor->setAlignment(align & 0x0F);
actor->setEnemyAlignment(align & 0xF0);
actor->setUnkByte(npcds->readByte()); // 0x0C: unknown;
// 0x0C is almost always zero, except for
// the avatar (0xC0) and
// Malchir, Vardion, Gorgrond, Beren (0xE0)
npcds->skip(14); // 0x0D-0x1A: unknown, always zero
actor->clearActorFlag(0xFF);
actor->setActorFlag(npcds->readByte()); // 0x1B: flags
npcds->skip(1); // 0x1C: unknown, always zero
npcds->skip(16); // 0x1D-0x2C: equipment
int16 mana = static_cast<int16>(npcds->readUint16LE()); // 0x2D,0x2E: mana
actor->setMana(mana);
actor->clearActorFlag(0xFFFF00);
uint32 flags2F = npcds->readByte(); // 0x2F: flags
actor->setActorFlag(flags2F << 8);
uint32 flags30 = npcds->readByte(); // 0x30: flags
actor->setActorFlag(flags30 << 16);
}
delete itemds;
delete npcds;
}
void World::worldStats() const {
unsigned int i, mapcount = 0;
for (i = 0; i < _maps.size(); i++) {
if (_maps[i] != nullptr && !_maps[i]->isEmpty())
mapcount++;
}
g_debugger->debugPrintf("World memory stats:\n");
g_debugger->debugPrintf("Maps : %u/256\n", mapcount);
const Actor *av = getMainActor();
g_debugger->debugPrintf("Avatar pos.: ");
if (av) {
g_debugger->debugPrintf("map %d, (", av->getMapNum());
Point3 pt = av->getLocation();
g_debugger->debugPrintf("%d,%d,%d)\n", pt.x, pt.y, pt.z);
} else {
g_debugger->debugPrintf("missing (null)\n");
}
}
void World::save(Common::WriteStream *ws) {
ws->writeUint32LE(_currentMap->getNum());
ws->writeUint16LE(_currentMap->_eggHatcher);
if (GAME_IS_CRUSADER) {
ws->writeByte(_alertActive ? 1 : 0);
ws->writeByte(_difficulty);
ws->writeUint16LE(_controlledNPCNum);
ws->writeUint32LE(_vargasShield);
}
uint16 es = static_cast<uint16>(_ethereal.size());
ws->writeUint32LE(es);
// empty stack and refill it again
uint16 *e = new uint16[es];
Std::list<ObjId>::const_iterator it = _ethereal.begin();
unsigned int i;
for (i = 0; i < es; ++i) {
e[es - i] = *it;
++it;
}
for (i = 0; i < es; ++i) {
ws->writeUint16LE(e[i]);
}
delete[] e;
}
// load items
bool World::load(Common::ReadStream *rs, uint32 version) {
uint16 curmapnum = rs->readUint32LE();
_currentMap->setMap(_maps[curmapnum]);
_currentMap->_eggHatcher = rs->readUint16LE();
if (GAME_IS_CRUSADER) {
_alertActive = (rs->readByte() != 0);
_difficulty = rs->readByte();
_controlledNPCNum = rs->readUint16LE();
_vargasShield = rs->readUint32LE();
}
uint32 etherealcount = rs->readUint32LE();
for (unsigned int i = 0; i < etherealcount; ++i) {
_ethereal.push_front(rs->readUint16LE());
}
return true;
}
void World::saveMaps(Common::WriteStream *ws) {
ws->writeUint32LE(static_cast<uint32>(_maps.size()));
for (unsigned int i = 0; i < _maps.size(); ++i) {
_maps[i]->save(ws);
}
}
bool World::loadMaps(Common::ReadStream *rs, uint32 version) {
uint32 mapcount = rs->readUint32LE();
// Integrity check
if (mapcount > _maps.size()) {
warning("Invalid mapcount in save: %d. Corrupt save?", mapcount);
return false;
}
// Map objects have already been created by reset()
for (unsigned int i = 0; i < mapcount; ++i) {
bool res = _maps[i]->load(rs, version);
if (!res) return false;
}
return true;
}
void World::setAlertActive(bool active)
{
assert(GAME_IS_CRUSADER);
_alertActive = active;
if (GAME_IS_REMORSE) {
setAlertActiveRemorse(active);
} else {
setAlertActiveRegret(active);
}
}
void World::setAlertActiveRemorse(bool active)
{
// Replicate the behavior of the original game.
LOOPSCRIPT(script,
LS_OR(
LS_OR(
LS_OR(
LS_OR(LS_SHAPE_EQUAL(0x49), LS_SHAPE_EQUAL(0x21)),
LS_SHAPE_EQUAL(0x174)),
LS_SHAPE_EQUAL(0x271)),
LS_SHAPE_EQUAL(0x477))
);
UCList itemlist(2);
_world->getCurrentMap()->areaSearch(&itemlist, script, sizeof(script),
nullptr, 0xffff, false);
for (uint32 i = 0; i < itemlist.getSize(); i++) {
uint16 itemid = itemlist.getuint16(i);
Item *item = getItem(itemid);
assert(item);
int frame = item->getFrame();
if (_alertActive) {
if (item->getShape() == 0x477) {
if (frame < 2)
item->setFrame(frame + 2);
} else if (frame == 0) {
item->setFrame(1);
}
} else {
if (item->getShape() == 0x477) {
if (frame > 1)
item->setFrame(frame - 2);
} else if (frame == 1) {
item->setFrame(0);
}
}
}
}
void World::setAlertActiveRegret(bool active)
{
setAlertActiveRemorse(active);
LOOPSCRIPT(offscript, LS_OR(LS_SHAPE_EQUAL(0x660), LS_SHAPE_EQUAL(0x661)));
LOOPSCRIPT(onscript, LS_OR(LS_SHAPE_EQUAL(0x662), LS_SHAPE_EQUAL(0x663)));
const uint8 *script = active ? onscript : offscript;
// note: size should be the same, but just to be explicit.
int scriptlen = active ? sizeof(onscript) : sizeof(offscript);
UCList itemlist(2);
_world->getCurrentMap()->areaSearch(&itemlist, script, scriptlen,
nullptr, 0xffff, false);
for (uint32 i = 0; i < itemlist.getSize(); i++) {
uint16 itemid = itemlist.getuint16(i);
Item *item = getItem(itemid);
assert(item);
switch (item->getShape()) {
case 0x660:
item->setShape(0x663);
break;
case 0x661:
item->setShape(0x662);
break;
case 0x662:
item->setShape(0x661);
break;
case 0x663:
item->setShape(0x660);
break;
default:
warning("unexpected shape %d returned from search", item->getShape());
break;
}
item->setFrame(0);
}
}
void World::setGameDifficulty(uint8 difficulty) {
_difficulty = difficulty;
if (GAME_IS_REMORSE) {
// HACK: Set ammo data for BA-40 in higher 2 difficulty levels
// This would be better handled in the ini file somehow?
const ShapeInfo *si = GameData::get_instance()->getMainShapes()->getShapeInfo(0x32E);
if (si && si->_weaponInfo) {
WeaponInfo *wi = si->_weaponInfo;
wi->_clipSize = 20;
if (difficulty > 1) {
wi->_ammoShape = 0x33D;
wi->_ammoType = 1;
} else {
wi->_ammoShape = 0;
wi->_ammoType = 0;
}
}
}
}
void World::setControlledNPCNum(uint16 num) {
uint16 oldnpc = _controlledNPCNum;
_controlledNPCNum = num;
Actor *previous = getActor(oldnpc);
if (previous && !previous->isDead() && previous->isInCombat()) {
previous->clearInCombat();
}
Actor *controlled = getActor(num);
if (controlled) {
if (num != 1) {
Kernel::get_instance()->killProcesses(num, Kernel::PROC_TYPE_ALL, true);
if (controlled->isInCombat())
controlled->clearInCombat();
}
Point3 pt = controlled->getCentre();
CameraProcess::SetCameraProcess(new CameraProcess(pt));
}
TargetReticleProcess *t = TargetReticleProcess::get_instance();
if (t) {
t->avatarMoved();
}
}
uint32 World::I_getAlertActive(const uint8 * /*args*/,
unsigned int /*argsize*/) {
return get_instance()->_world->isAlertActive() ? 1 : 0;
}
uint32 World::I_setAlertActive(const uint8 * /*args*/,
unsigned int /*argsize*/) {
get_instance()->_world->setAlertActive(true);
return 0;
}
uint32 World::I_clrAlertActive(const uint8 * /*args*/,
unsigned int /*argsize*/) {
get_instance()->_world->setAlertActive(false);
return 0;
}
uint32 World::I_gameDifficulty(const uint8 * /*args*/,
unsigned int /*argsize*/) {
return get_instance()->_world->getGameDifficulty();
}
uint32 World::I_getControlledNPCNum(const uint8 * /*args*/,
unsigned int /*argsize*/) {
return get_instance()->_world->getControlledNPCNum();
}
uint32 World::I_setControlledNPCNum(const uint8 *args,
unsigned int /*argsize*/) {
ARG_UINT16(num);
get_instance()->_world->setControlledNPCNum(num);
return 0;
}
uint32 World::I_resetVargasShield(const uint8 * /*args*/,
unsigned int /*argsize*/) {
get_instance()->setVargasShield(500);
return 0;
}
} // End of namespace Ultima8
} // End of namespace Ultima