You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
593 lines
14 KiB
D
593 lines
14 KiB
D
/*
|
|
OpenMW - The completely unofficial reimplementation of Morrowind
|
|
Copyright (C) 2008 Nicolay Korslund
|
|
Email: < korslund@gmail.com >
|
|
WWW: http://openmw.snaptoad.com/
|
|
|
|
This file (celldata.d) is part of the OpenMW package.
|
|
|
|
OpenMW is distributed as free software: you can redistribute it
|
|
and/or modify it under the terms of the GNU General Public License
|
|
version 3, as published by the Free Software Foundation.
|
|
|
|
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
|
|
version 3 along with this program. If not, see
|
|
http://www.gnu.org/licenses/ .
|
|
|
|
*/
|
|
|
|
module scene.celldata;
|
|
|
|
import std.stdio;
|
|
|
|
public import esm.esmmain;
|
|
|
|
import core.memory;
|
|
import monster.monster;
|
|
|
|
import util.reglist;
|
|
|
|
import ogre.ogre;
|
|
import ogre.bindings;
|
|
|
|
import sound.audio;
|
|
|
|
import scene.player;
|
|
|
|
// Generic version of a "live" object. Do we even need this at all?
|
|
// No, I don't think so.
|
|
struct GenLive(T)
|
|
{
|
|
// Instance of class GameObject or a derived class (depending on
|
|
// object type)
|
|
MonsterObject *obj;
|
|
T *m;
|
|
|
|
Placement *getPos()
|
|
{
|
|
assert(obj !is null);
|
|
// This belongs in HACK-land
|
|
return cast(Placement*)obj.getFloatPtr("x");
|
|
}
|
|
|
|
float getScale()
|
|
{
|
|
assert(obj !is null);
|
|
return obj.getFloat("scale");
|
|
}
|
|
}
|
|
|
|
alias GenLive!(Static) LiveStatic;
|
|
alias GenLive!(NPC) LiveNPC;
|
|
alias GenLive!(Activator) LiveActivator;
|
|
alias GenLive!(Potion) LivePotion;
|
|
alias GenLive!(Apparatus) LiveApparatus;
|
|
alias GenLive!(Ingredient) LiveIngredient;
|
|
alias GenLive!(Armor) LiveArmor;
|
|
alias GenLive!(Weapon) LiveWeapon;
|
|
alias GenLive!(Book) LiveBook;
|
|
alias GenLive!(Clothing) LiveClothing;
|
|
alias GenLive!(Tool) LiveTool;
|
|
alias GenLive!(Creature) LiveCreature;
|
|
alias GenLive!(Door) LiveDoor;
|
|
alias GenLive!(Misc) LiveMisc;
|
|
alias GenLive!(Container) LiveContainer;
|
|
|
|
// TODO: This is sort of redundant. Eliminate or rework it later.
|
|
struct LiveLight
|
|
{
|
|
MonsterObject *obj;
|
|
Light *m;
|
|
|
|
Placement *getPos()
|
|
{
|
|
assert(obj !is null);
|
|
// This belongs in HACK-land
|
|
return cast(Placement*)obj.getFloatPtr("x");
|
|
}
|
|
|
|
float getScale()
|
|
{
|
|
assert(obj !is null);
|
|
return obj.getFloat("scale");
|
|
}
|
|
|
|
NodePtr lightNode;
|
|
SoundInstance *loopSound;
|
|
}
|
|
|
|
class CellData
|
|
{
|
|
private:
|
|
RegionManager reg;
|
|
|
|
public:
|
|
|
|
InteriorCell *inCell;
|
|
ExteriorCell *exCell;
|
|
|
|
// Ambient light data
|
|
AMBIStruct ambi;
|
|
|
|
// Water height
|
|
int water;
|
|
|
|
// Linked lists that hold references to all the objects in the cell
|
|
RegionList!(LiveStatic) statics;
|
|
RegionList!(LiveMisc) miscItems;
|
|
RegionList!(LiveLight) lights;
|
|
RegionList!(LiveLight) statLights;
|
|
RegionList!(LiveNPC) npcs;
|
|
RegionList!(LiveContainer) containers;
|
|
RegionList!(LiveDoor) doors;
|
|
RegionList!(LiveActivator) activators;
|
|
RegionList!(LivePotion) potions;
|
|
RegionList!(LiveApparatus) appas;
|
|
RegionList!(LiveIngredient) ingredients;
|
|
RegionList!(LiveArmor) armors;
|
|
RegionList!(LiveWeapon) weapons;
|
|
RegionList!(LiveBook) books;
|
|
RegionList!(LiveTool) tools;
|
|
RegionList!(LiveClothing) clothes;
|
|
RegionList!(LiveCreature) creatures;
|
|
|
|
this(RegionManager r)
|
|
{
|
|
reg = r;
|
|
killCell(); // Make sure all data is initialized.
|
|
}
|
|
|
|
// Kills all data and initialize the object for reuse.
|
|
void killCell()
|
|
{
|
|
inCell = null;
|
|
exCell = null;
|
|
|
|
// Reset the lists
|
|
statics.init(reg);
|
|
miscItems.init(reg);
|
|
lights.init(reg);
|
|
statLights.init(reg);
|
|
npcs.init(reg);
|
|
containers.init(reg);
|
|
doors.init(reg);
|
|
activators.init(reg);
|
|
potions.init(reg);
|
|
appas.init(reg);
|
|
ingredients.init(reg);
|
|
armors.init(reg);
|
|
weapons.init(reg);
|
|
books.init(reg);
|
|
tools.init(reg);
|
|
clothes.init(reg);
|
|
creatures.init(reg);
|
|
|
|
// Write some statistics
|
|
//writefln(reg);
|
|
|
|
reg.freeAll();
|
|
}
|
|
|
|
// Load an exterior cell
|
|
void loadExtCell(int x, int y)
|
|
{
|
|
exCell = cells.getExt(x, y);
|
|
|
|
writefln("Name: %s", exCell.name);
|
|
writefln("Region: %s", exCell.region.id);
|
|
|
|
esFile.restoreContext(exCell.context, reg);
|
|
|
|
// Always for exterior cells
|
|
water = 0;
|
|
|
|
Color mapColor;
|
|
if(esFile.isNextSub("NAM5"))
|
|
esFile.readHExact(&mapColor, mapColor.sizeof);
|
|
|
|
loadReferences();
|
|
|
|
const float cellWidth = 8192;
|
|
|
|
// TODO/FIXME: This is temporary
|
|
playerData.position.position[0] = x*cellWidth;
|
|
playerData.position.position[1] = y*cellWidth;
|
|
playerData.position.position[2] = 6000;
|
|
playerData.position.rotation[] = 0;
|
|
}
|
|
|
|
// Load an interior cell
|
|
void loadIntCell(char[] cName)
|
|
{
|
|
inCell = cells.getInt(cName);
|
|
/*
|
|
writefln("Cell id: '%s'", cName);
|
|
writefln("Cell name: '%s'", cell.id);
|
|
*/
|
|
|
|
esFile.restoreContext(inCell.context, reg);
|
|
|
|
// TODO: Read this in loadcell.d
|
|
if(esFile.isNextSub("INTV") || esFile.isNextSub("WHGT"))
|
|
water = esFile.getHInt();
|
|
//writefln("Water height: ", water);
|
|
|
|
if(inCell.flags & CellFlags.QuasiExt)
|
|
{
|
|
Region* reg = esFile.getHNOPtr!(Region)("RGNN", regions);
|
|
if(reg) writefln("Cell has region %s", reg.id);
|
|
else writefln("No region");
|
|
// Determine weather from this region
|
|
}
|
|
else
|
|
{
|
|
// Only for interior cells
|
|
esFile.readHNExact(&ambi, ambi.sizeof, "AMBI");
|
|
}
|
|
|
|
/*
|
|
writefln("Ambient light: ", ambi.ambient.array);
|
|
writefln("Sunlight: ", ambi.sunlight.array);
|
|
writefln("Fog color: ", ambi.ambient.array);
|
|
writefln("Fog density: ", ambi.fogDensity);
|
|
*/
|
|
loadReferences();
|
|
}
|
|
|
|
private:
|
|
|
|
void loadReferences()
|
|
{
|
|
with(esFile)
|
|
{
|
|
|
|
// Now read all the references
|
|
while(hasMoreSubs)
|
|
{
|
|
// Number of references in the cell? Maximum once in each
|
|
// cell, but not always at the beginning, and not always
|
|
// right. In other words, completely useless. Strange
|
|
// people...
|
|
getHNOInt("NAM0", 0);
|
|
|
|
int refnum = getHNInt("FRMR"); // Reference number
|
|
char[] refr = getHNString("NAME"); // ID of object
|
|
|
|
// Used internally for optimizing
|
|
bool container;
|
|
bool door;
|
|
bool stat;
|
|
bool activator;
|
|
|
|
// Identify the referenced object by looking up the id
|
|
// string 'ref' in the global database of all
|
|
// cell-referencable objects.
|
|
Item itm = cellRefs.lookup(refr);
|
|
ItemT it = ItemT(itm);
|
|
|
|
MonsterObject *mo;
|
|
|
|
// These should be ordered according to how commonly they
|
|
// occur.
|
|
|
|
// Static mesh - probably the most common object type
|
|
if(Static *s = it.getStatic())
|
|
{
|
|
LiveStatic ls;
|
|
ls.m = s;
|
|
ls.obj = s.proto.clone();
|
|
mo = ls.obj;
|
|
statics.insert(ls);
|
|
stat = true;
|
|
}
|
|
// Misc items are also pretty common
|
|
else if(Misc *m = it.getMisc())
|
|
{
|
|
LiveMisc ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
miscItems.insert(ls);
|
|
}
|
|
// Lights and containers too
|
|
else if(Light *m = it.getLight())
|
|
{
|
|
LiveLight ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
|
|
bool carry = (m.data.flags&Light.Flags.Carry) != 0;
|
|
if(carry)
|
|
lights.insert(ls);
|
|
else
|
|
statLights.insert(ls);
|
|
}
|
|
else if(Container *c = it.getContainer())
|
|
{
|
|
LiveContainer ls;
|
|
ls.m = c;
|
|
ls.obj = c.proto.clone();
|
|
mo = ls.obj;
|
|
containers.insert(ls);
|
|
container = true;
|
|
}
|
|
// From here on I doubt the order will matter much
|
|
else if(Door *d = it.getDoor())
|
|
{
|
|
LiveDoor ls;
|
|
ls.m = d;
|
|
ls.obj = d.proto.clone();
|
|
mo = ls.obj;
|
|
doors.insert(ls);
|
|
door = true;
|
|
}
|
|
// Activator?
|
|
else if(Activator *a = it.getActivator())
|
|
{
|
|
LiveActivator ls;
|
|
ls.m = a;
|
|
ls.obj = a.proto.clone();
|
|
mo = ls.obj;
|
|
activators.insert(ls);
|
|
activator = true;
|
|
}
|
|
// NPC?
|
|
else if(NPC *n = it.getNPC())
|
|
{
|
|
LiveNPC ls;
|
|
ls.m = n;
|
|
ls.obj = n.proto.clone();
|
|
mo = ls.obj;
|
|
npcs.insert(ls);
|
|
}
|
|
else if(Potion *p = it.getPotion())
|
|
{
|
|
LivePotion ls;
|
|
ls.m = p;
|
|
ls.obj = p.proto.clone();
|
|
mo = ls.obj;
|
|
potions.insert(ls);
|
|
}
|
|
else if(Apparatus *m = it.getApparatus())
|
|
{
|
|
LiveApparatus ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
appas.insert(ls);
|
|
}
|
|
else if(Ingredient *m = it.getIngredient())
|
|
{
|
|
LiveIngredient ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
ingredients.insert(ls);
|
|
}
|
|
else if(Armor *m = it.getArmor())
|
|
{
|
|
LiveArmor ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
armors.insert(ls);
|
|
}
|
|
else if(Weapon *m = it.getWeapon())
|
|
{
|
|
LiveWeapon ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
weapons.insert(ls);
|
|
}
|
|
else if(Book *m = it.getBook())
|
|
{
|
|
LiveBook ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
books.insert(ls);
|
|
}
|
|
else if(Clothing *m = it.getClothing())
|
|
{
|
|
LiveClothing ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
clothes.insert(ls);
|
|
}
|
|
else if(Tool *m = it.getPick())
|
|
{
|
|
LiveTool ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
tools.insert(ls);
|
|
}
|
|
else if(Tool *m = it.getProbe())
|
|
{
|
|
LiveTool ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
tools.insert(ls);
|
|
}
|
|
else if(Tool *m = it.getRepair())
|
|
{
|
|
LiveTool ls;
|
|
ls.m = m;
|
|
ls.obj = m.proto.clone();
|
|
mo = ls.obj;
|
|
tools.insert(ls);
|
|
}
|
|
else if(Creature *c = it.getCreature())
|
|
{
|
|
LiveCreature ls;
|
|
ls.m = c;
|
|
ls.obj = c.proto.clone();
|
|
mo = ls.obj;
|
|
creatures.insert(ls);
|
|
}
|
|
else if(LeveledCreatures *l = it.getCreatureList)
|
|
{
|
|
// Create a creature, based on current player level.
|
|
LiveCreature ls;
|
|
ls.m = l.instCreature(*playerData.level);
|
|
if(ls.m != null)
|
|
{
|
|
// Note that this clones a creature object, not a
|
|
// leveled list object.
|
|
ls.obj = ls.m.proto.clone(); mo = ls.obj;
|
|
creatures.insert(ls);
|
|
}
|
|
}
|
|
else fail(format(" UNKNOWN REFERENCE! Type ", cast(int)it.i.type));
|
|
|
|
// Now that the object has found it's place, load data
|
|
// into the object.
|
|
|
|
with(*mo)
|
|
{
|
|
// Scale. Multiply with the existing scale value.
|
|
float *scale = getFloatPtr("scale");
|
|
*scale *= getHNOFloat("XSCL", 1.0);
|
|
|
|
// Statics only need the position data. Skip the rest
|
|
// as an optimization.
|
|
if(stat) goto readpos;
|
|
|
|
// The NPC that owns this object (and will get angry
|
|
// if you steal it)
|
|
setString8("owner", getHNOString("ANAM"));
|
|
|
|
// I have no idea, link to a global variable perhaps?
|
|
setString8("glob", getHNOString("BNAM"));
|
|
|
|
// ID of creature trapped in a soul gem (?)
|
|
setString8("soulID", getHNOString("XSOL"));
|
|
|
|
// ?? CNAM has a faction name, might be for
|
|
// objects/beds etc belonging to a faction.
|
|
{
|
|
char[] cnam = getHNOString("CNAM");
|
|
setString8("cnam", cnam);
|
|
|
|
// INDX might be PC faction rank required to use the
|
|
// item? Sometimes is -1.
|
|
if(cnam.length) setInt("indx", getHNInt("INDX"));
|
|
}
|
|
|
|
// Possibly weapon health, number of uses left or
|
|
// weapon magic charge?
|
|
setFloat("xchg", getHNOFloat("XCHG", 0.0));
|
|
|
|
// I have no idea, these are present some times, often
|
|
// along with owner (ANAM) and sometimes otherwise. Is
|
|
// NAM9 is always 1? INTV is usually one, but big for
|
|
// lights. Perhaps something to do with remaining
|
|
// light "charge". I haven't tried reading it as a
|
|
// float in those cases.
|
|
setInt("intv", getHNOInt("INTV", 0));
|
|
setInt("nam9", getHNOInt("NAM9", 0));
|
|
|
|
// Present for doors that teleport you to another
|
|
// cell.
|
|
if(door && isNextSub("DODT"))
|
|
{
|
|
setBool("teleport", true);
|
|
|
|
// Warning, HACK! Will be fixed when we implement
|
|
// structs in Monster.
|
|
Placement *p = cast(Placement*)getFloatPtr("destx");
|
|
readHExact(p, Placement.sizeof);
|
|
|
|
// Destination cell (optional?)
|
|
setString8("destCell", getHNOString("DNAM"));
|
|
}
|
|
|
|
if(door || container)
|
|
{
|
|
// Lock level (I think)
|
|
setInt("lockLevel", getHNOInt("FLTV", 0));
|
|
|
|
// For locked doors and containers
|
|
setString8("key", getHNOString("KNAM"));
|
|
setString8("trap", getHNOString("TNAM"));
|
|
}
|
|
|
|
if(activator)
|
|
{
|
|
// Occurs ONCE in Morrowind.esm, for an activator.
|
|
setInt("unam", getHNOByte("UNAM", 0));
|
|
|
|
// Occurs in Tribunal.esm, eg. in the cell
|
|
// "Mournhold, Plaza Brindisi Dorom", where it has
|
|
// the value 100.
|
|
setInt("fltv", getHNOInt("FLTV", 0));
|
|
}
|
|
|
|
readpos:
|
|
|
|
// Position and rotation of this object within the
|
|
// cell
|
|
|
|
// TODO: This is a HACK. It assumes the class variable
|
|
// floats are placed consecutively in memory in the
|
|
// right order. This is true, but is a very bug prone
|
|
// method of doing this. Will fix when Monster gets
|
|
// structs. (See the DODT record above also.)
|
|
Placement *pos = cast(Placement*)getFloatPtr("x");
|
|
readHNExact(pos, Placement.sizeof, "DATA");
|
|
|
|
// TODO/FIXME: Very temporary. Set player position at
|
|
// the first door we find.
|
|
if(door && !playerData.posSet)
|
|
{
|
|
playerData.posSet = true;
|
|
*playerData.position = *pos;
|
|
}
|
|
}
|
|
} // End of while(hasMoreSubs)
|
|
}
|
|
|
|
} // End of loadReferences()
|
|
} // End of class CellData
|
|
|
|
CellFreelist cellList;
|
|
|
|
// Structure used as a free list for cell data objects and their
|
|
// respective regions.
|
|
struct CellFreelist
|
|
{
|
|
// We love static arrays! And 100 cells should be enough for
|
|
// everybody :)
|
|
CellData[100] list;
|
|
uint next;
|
|
|
|
// TODO: Figure out a good size to use here as well.
|
|
CellData get()
|
|
{
|
|
if(next) return list[--next];
|
|
|
|
// Since these are reused, there's no waste in allocating on the
|
|
// heap here. Also, this is only semi-runtime (executed when
|
|
// loading a cell), thus an occational GC slow down is not
|
|
// critical.
|
|
return new CellData(new RegionManager("CELL"));
|
|
}
|
|
|
|
void release(CellData r)
|
|
{
|
|
assert(next < list.length);
|
|
|
|
r.killCell();
|
|
list[next++] = r;
|
|
}
|
|
}
|