removing last bits of old D code
parent
f14dc90b34
commit
6ada405af0
@ -1,66 +0,0 @@
|
||||
|
||||
# /
|
||||
/cache
|
||||
/later
|
||||
/openmw.ini.*
|
||||
/rr.sh
|
||||
/fontdump
|
||||
/MyGUI.log
|
||||
/upm.sh
|
||||
/raw.txt
|
||||
/vids
|
||||
/include
|
||||
/includes
|
||||
/.thumbnails
|
||||
/*.jpg
|
||||
/*.dll
|
||||
/*.exe
|
||||
/*.def
|
||||
/*.a
|
||||
/*.map
|
||||
/*.rsp
|
||||
/ogre.cfg
|
||||
/openmw
|
||||
/bored
|
||||
/bsatool
|
||||
/niftool
|
||||
/esmtool
|
||||
/bored.highscores
|
||||
/Ogre.log
|
||||
/openmw.ini
|
||||
/openmw.ini.old
|
||||
/dsss_*
|
||||
/dsss.last
|
||||
/objs
|
||||
/nifobjs
|
||||
|
||||
# /bullet/
|
||||
/bullet/OgreOpcode*
|
||||
/bullet/demo
|
||||
/bullet/*.a
|
||||
|
||||
# /media_mygui/
|
||||
/media_mygui/core.skin.orig
|
||||
/media_mygui/.thumbnails
|
||||
|
||||
# /monster/
|
||||
/monster/*openmw_last
|
||||
|
||||
# /mscripts/
|
||||
/mscripts/draft
|
||||
|
||||
# /nif/
|
||||
/nif/bumpmap
|
||||
/nif/*.nif
|
||||
|
||||
# /ogre/
|
||||
/ogre/*.nif
|
||||
/ogre/cs
|
||||
|
||||
# /util/
|
||||
/util/iconv
|
||||
|
||||
*.o
|
||||
*.patch
|
||||
*.diff
|
||||
.directory
|
@ -1,85 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (bindings.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 bullet.bindings;
|
||||
|
||||
/*
|
||||
* This module is the interface between D and the C++ code that
|
||||
* handles Bullet.
|
||||
*/
|
||||
|
||||
typedef void* BulletShape;
|
||||
|
||||
extern(C):
|
||||
|
||||
// Initialize the dynamic world. Returns non-zero if an error occurs.
|
||||
int bullet_init();
|
||||
|
||||
// Set physics modes
|
||||
void bullet_nextMode();
|
||||
void bullet_walk();
|
||||
void bullet_fly();
|
||||
void bullet_ghost();
|
||||
|
||||
// Warp the player to a specific location.
|
||||
void bullet_movePlayer(float x, float y, float z);
|
||||
|
||||
// Request that the player moves in this direction
|
||||
void bullet_setPlayerDir(float x, float y, float z);
|
||||
|
||||
// Get the current player position, after physics and collision have
|
||||
// been applied.
|
||||
void bullet_getPlayerPos(float *x, float *y, float *z);
|
||||
|
||||
// Create a box shape. Used for bounding boxes. The box is a trimesh
|
||||
// and is hollow (you can walk inside it.)
|
||||
void bullet_createBoxShape(float minX, float minY, float minZ,
|
||||
float maxX, float maxY, float maxZ,
|
||||
float *trans,float *matrix);
|
||||
|
||||
// Create a triangle shape. This is cumulative, all meshes created
|
||||
// with this function are added to the same shape. Since the various
|
||||
// parts of a mesh can be differently transformed and we are putting
|
||||
// them all in one shape, we must transform the vertices manually.
|
||||
void bullet_createTriShape(int numFaces,
|
||||
void *triArray,
|
||||
int numVerts,
|
||||
void *vertArray,
|
||||
float *trans,float *matrix);
|
||||
|
||||
// "Flushes" the meshes created with createTriShape, returning the
|
||||
// pointer to the final shape object.
|
||||
BulletShape bullet_getFinalShape();
|
||||
|
||||
// Insert a static mesh with the given translation, quaternion
|
||||
// rotation and scale. The quaternion is assumed to be in Ogre format,
|
||||
// ie. with the W first.
|
||||
void bullet_insertStatic(BulletShape shp, float *pos,
|
||||
float *quat, float scale);
|
||||
|
||||
// Move the physics simulation 'delta' seconds forward in time
|
||||
void bullet_timeStep(float delta);
|
||||
|
||||
// Deallocate objects
|
||||
void bullet_cleanup();
|
||||
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (bullet.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 bullet.bullet;
|
||||
|
||||
import bullet.bindings;
|
||||
|
||||
void initBullet()
|
||||
{
|
||||
if(bullet_init())
|
||||
throw new Exception("Bullet setup failed");
|
||||
}
|
||||
|
||||
void cleanupBullet() { bullet_cleanup(); }
|
@ -1,502 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (cpp_bullet.cpp) 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/ .
|
||||
|
||||
*/
|
||||
|
||||
#include "btBulletDynamicsCommon.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "../util/dbg.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
class CustomOverlappingPairCallback;
|
||||
|
||||
enum
|
||||
{
|
||||
MASK_PLAYER = 1,
|
||||
MASK_STATIC = 2
|
||||
};
|
||||
|
||||
// System variables
|
||||
btDefaultCollisionConfiguration* g_collisionConfiguration;
|
||||
btCollisionDispatcher *g_dispatcher;
|
||||
//btBroadphaseInterface *g_broadphase;
|
||||
btAxisSweep3 *g_broadphase;
|
||||
btSequentialImpulseConstraintSolver* g_solver;
|
||||
btDynamicsWorld *g_dynamicsWorld;
|
||||
|
||||
// Player variables
|
||||
btCollisionObject* g_playerObject;
|
||||
btConvexShape *g_playerShape;
|
||||
|
||||
// Player position. This is updated automatically by the physics
|
||||
// system based on g_walkDirection and collisions. It is read by D
|
||||
// code through bullet_getPlayerPos().
|
||||
btVector3 g_playerPosition;
|
||||
|
||||
// Walking vector - defines direction and speed that the player
|
||||
// intends to move right now. This is updated from D code each frame
|
||||
// through bullet_setPlayerDir(), based on player input (and later, AI
|
||||
// decisions.) The units of the vector are points per second.
|
||||
btVector3 g_walkDirection;
|
||||
|
||||
// The current trimesh shape being built. All new inserted meshes are
|
||||
// added into this, until bullet_getFinalShape() is called.
|
||||
btTriangleIndexVertexArray *g_currentMesh;
|
||||
|
||||
// These variables and the class below are used in player collision
|
||||
// detection. The callback is injected into the broadphase and keeps a
|
||||
// continuously updated list of what objects are colliding with the
|
||||
// player (in g_pairCache). This list is used in the function called
|
||||
// recoverFromPenetration().
|
||||
btHashedOverlappingPairCache* g_pairCache;
|
||||
CustomOverlappingPairCallback *g_customPairCallback;
|
||||
|
||||
// Three physics modes: walking (with gravity and collision), flying
|
||||
// (collision but no gravity) and ghost mode (fly through walls)
|
||||
enum
|
||||
{
|
||||
PHYS_WALK,
|
||||
PHYS_FLY,
|
||||
PHYS_GHOST
|
||||
};
|
||||
int g_physMode;
|
||||
|
||||
// Include the player physics
|
||||
#include "cpp_player.cpp"
|
||||
|
||||
// Include the uniform shape scaler
|
||||
#include "cpp_scale.cpp"
|
||||
|
||||
class CustomOverlappingPairCallback : public btOverlappingPairCallback
|
||||
{
|
||||
public:
|
||||
virtual btBroadphasePair* addOverlappingPair(btBroadphaseProxy* proxy0,
|
||||
btBroadphaseProxy* proxy1)
|
||||
{
|
||||
if (proxy0->m_clientObject==g_playerObject ||
|
||||
proxy1->m_clientObject==g_playerObject)
|
||||
return g_pairCache->addOverlappingPair(proxy0,proxy1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual void* removeOverlappingPair(btBroadphaseProxy* proxy0,
|
||||
btBroadphaseProxy* proxy1,
|
||||
btDispatcher* dispatcher)
|
||||
{
|
||||
if (proxy0->m_clientObject==g_playerObject ||
|
||||
proxy1->m_clientObject==g_playerObject)
|
||||
return g_pairCache->removeOverlappingPair(proxy0,proxy1,dispatcher);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void removeOverlappingPairsContainingProxy(btBroadphaseProxy* proxy0,
|
||||
btDispatcher* dispatcher)
|
||||
{ if (proxy0->m_clientObject==g_playerObject)
|
||||
g_pairCache->removeOverlappingPairsContainingProxy(proxy0,dispatcher);
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" int32_t bullet_init()
|
||||
{
|
||||
// ------- SET UP THE WORLD -------
|
||||
|
||||
// Set up basic objects
|
||||
g_collisionConfiguration = new btDefaultCollisionConfiguration();
|
||||
g_dispatcher = new btCollisionDispatcher(g_collisionConfiguration);
|
||||
//g_broadphase = new btDbvtBroadphase();
|
||||
g_solver = new btSequentialImpulseConstraintSolver;
|
||||
|
||||
// TODO: Figure out what to do with this. We need the user callback
|
||||
// function used below (I think), but this is only offered by this
|
||||
// broadphase implementation (as far as I can see.) Maybe we can
|
||||
// scan through the cell first and find good values that covers all
|
||||
// the objects before we set up the dynamic world. Another option is
|
||||
// to create a custom broadphase designed for our purpose. (We
|
||||
// should probably use different ones for interior and exterior
|
||||
// cells in any case.)
|
||||
btVector3 worldMin(-20000,-20000,-20000);
|
||||
btVector3 worldMax(20000,20000,20000);
|
||||
g_broadphase = new btAxisSweep3(worldMin,worldMax);
|
||||
|
||||
g_dynamicsWorld =
|
||||
new btDiscreteDynamicsWorld(g_dispatcher,
|
||||
g_broadphase,
|
||||
g_solver,
|
||||
g_collisionConfiguration);
|
||||
|
||||
//g_dynamicsWorld->setGravity(btVector3(0,-10,0));
|
||||
|
||||
|
||||
// ------- SET UP THE PLAYER -------
|
||||
|
||||
// Create the player collision shape.
|
||||
float width = 30;
|
||||
|
||||
/*
|
||||
float height = 50;
|
||||
btVector3 spherePositions[2];
|
||||
btScalar sphereRadii[2];
|
||||
sphereRadii[0] = width;
|
||||
sphereRadii[1] = width;
|
||||
spherePositions[0] = btVector3 (0,0,0);
|
||||
spherePositions[1] = btVector3 (0,0,-height);
|
||||
|
||||
// One possible shape is the convex hull around two spheres
|
||||
g_playerShape = new btMultiSphereShape(btVector3(width/2.0, height/2.0,
|
||||
width/2.0), &spherePositions[0], &sphereRadii[0], 2);
|
||||
*/
|
||||
|
||||
// Other posibilities - most are too slow, except the sphere
|
||||
//g_playerShape = new btCylinderShapeZ(btVector3(width, width, height));
|
||||
g_playerShape = new btSphereShape(width);
|
||||
//g_playerShape = new btCapsuleShapeZ(width, height);
|
||||
|
||||
// Create the collision object
|
||||
g_playerObject = new btCollisionObject ();
|
||||
g_playerObject->setCollisionShape (g_playerShape);
|
||||
g_playerObject->setCollisionFlags (btCollisionObject::CF_NO_CONTACT_RESPONSE);
|
||||
|
||||
|
||||
// ------- OTHER STUFF -------
|
||||
|
||||
// Create a custom callback to pick out all the objects colliding
|
||||
// with the player. We use this in the collision recovery phase.
|
||||
g_pairCache = new btHashedOverlappingPairCache();
|
||||
g_customPairCallback = new CustomOverlappingPairCallback();
|
||||
g_broadphase->setOverlappingPairUserCallback(g_customPairCallback);
|
||||
|
||||
// Set up the callback that moves the player at the end of each
|
||||
// simulation step.
|
||||
g_dynamicsWorld->setInternalTickCallback(playerStepCallback);
|
||||
|
||||
// Add the character collision object to the world.
|
||||
g_dynamicsWorld->addCollisionObject(g_playerObject,
|
||||
MASK_PLAYER,
|
||||
MASK_STATIC);
|
||||
|
||||
// Make sure these is zero at startup
|
||||
g_currentMesh = NULL;
|
||||
|
||||
// Start out walking
|
||||
g_physMode = PHYS_WALK;
|
||||
|
||||
// Success!
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set physics modes
|
||||
extern "C" void bullet_walk()
|
||||
{
|
||||
g_physMode = PHYS_WALK;
|
||||
cout << "Walk mode\n";
|
||||
}
|
||||
|
||||
extern "C" void bullet_fly()
|
||||
{
|
||||
g_physMode = PHYS_FLY;
|
||||
cout << "Fly mode\n";
|
||||
}
|
||||
|
||||
extern "C" void bullet_ghost()
|
||||
{
|
||||
g_physMode = PHYS_GHOST;
|
||||
cout << "Ghost mode\n";
|
||||
}
|
||||
|
||||
// Switch to the next physics mode
|
||||
extern "C" void bullet_nextMode()
|
||||
{
|
||||
switch(g_physMode)
|
||||
{
|
||||
case PHYS_WALK:
|
||||
bullet_fly();
|
||||
break;
|
||||
case PHYS_FLY:
|
||||
bullet_ghost();
|
||||
break;
|
||||
case PHYS_GHOST:
|
||||
bullet_walk();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Warp the player to a specific location. We do not bother setting
|
||||
// rotation, since it's completely irrelevant for collision detection,
|
||||
// and doubly so since the collision mesh is a sphere.
|
||||
extern "C" void bullet_movePlayer(float x, float y, float z)
|
||||
{
|
||||
btTransform tr;
|
||||
tr.setIdentity();
|
||||
tr.setOrigin(btVector3(x,y,z));
|
||||
g_playerObject->setWorldTransform(tr);
|
||||
}
|
||||
|
||||
// Request that the player moves in this direction
|
||||
extern "C" void bullet_setPlayerDir(float x, float y, float z)
|
||||
{ g_walkDirection.setValue(x,y,z); }
|
||||
|
||||
// Get the current player position, after physics and collision have
|
||||
// been applied.
|
||||
extern "C" void bullet_getPlayerPos(float *x, float *y, float *z)
|
||||
{
|
||||
*x = g_playerPosition.getX();
|
||||
*y = g_playerPosition.getY();
|
||||
*z = g_playerPosition.getZ();
|
||||
}
|
||||
|
||||
void* copyBuffer(const void *buf, int elemSize, int len)
|
||||
{
|
||||
int size = elemSize * len;
|
||||
void *res = malloc(size);
|
||||
memcpy(res, buf, size);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Internal version that does not copy buffers
|
||||
void createTriShape(int32_t numFaces, const void *triArray,
|
||||
int32_t numVerts, const void *vertArray,
|
||||
const float *trans, const float *matrix)
|
||||
{
|
||||
// This struct holds the index and vertex buffers of a single
|
||||
// trimesh.
|
||||
btIndexedMesh im;
|
||||
|
||||
// Set up the triangles
|
||||
int numTriangles = numFaces / 3;
|
||||
im.m_numTriangles = numTriangles;
|
||||
im.m_triangleIndexStride = 6; // 3 indices * 2 bytes per short
|
||||
im.m_triangleIndexBase = (unsigned char*)triArray;
|
||||
|
||||
// Set up the vertices
|
||||
im.m_numVertices = numVerts;
|
||||
im.m_vertexStride = 12; // 4 bytes per float * 3 floats per vertex
|
||||
im.m_vertexBase = (unsigned char*)vertArray;
|
||||
|
||||
// Transform vertex values in vb according to 'trans' and 'matrix'
|
||||
float *vb = (float*)im.m_vertexBase;
|
||||
for(int i=0; i<numVerts; i++)
|
||||
{
|
||||
float x,y,z;
|
||||
|
||||
// Reinventing basic linear algebra for the win!
|
||||
x = matrix[0]*vb[0]+matrix[1]*vb[1]+matrix[2]*vb[2] + trans[0];
|
||||
y = matrix[3]*vb[0]+matrix[4]*vb[1]+matrix[5]*vb[2] + trans[1];
|
||||
z = matrix[6]*vb[0]+matrix[7]*vb[1]+matrix[8]*vb[2] + trans[2];
|
||||
*(vb++) = x;
|
||||
*(vb++) = y;
|
||||
*(vb++) = z;
|
||||
}
|
||||
|
||||
// If no mesh is currently active, create one
|
||||
if(g_currentMesh == NULL)
|
||||
g_currentMesh = new btTriangleIndexVertexArray;
|
||||
|
||||
// Add the mesh. Nif data stores triangle indices as shorts.
|
||||
g_currentMesh->addIndexedMesh(im, PHY_SHORT);
|
||||
}
|
||||
|
||||
// Define a cube with coordinates 0,0,0 - 1,1,1.
|
||||
const float cube_verts[] =
|
||||
{
|
||||
0,0,0, 1,0,0, 0,1,0,
|
||||
1,1,0, 0,0,1, 1,0,1,
|
||||
0,1,1, 1,1,1
|
||||
};
|
||||
|
||||
// Triangles of the cube. The orientation of each triange doesn't
|
||||
// matter.
|
||||
const short cube_tris[] =
|
||||
{
|
||||
// bottom side
|
||||
0, 1, 2,
|
||||
1, 2, 3,
|
||||
// top side
|
||||
4, 5, 6,
|
||||
5, 6, 7,
|
||||
// front side
|
||||
0, 4, 5,
|
||||
0, 1, 5,
|
||||
// back side
|
||||
2, 3, 7,
|
||||
2, 6, 7,
|
||||
// left side
|
||||
0, 2, 4,
|
||||
2, 4, 6,
|
||||
// right side
|
||||
1, 3, 5,
|
||||
3, 5, 7
|
||||
};
|
||||
|
||||
const int cube_num_verts = 8;
|
||||
const int cube_num_tris = 12;
|
||||
|
||||
// Create a (trimesh) box with the given dimensions. Used for bounding
|
||||
// boxes. TODO: I guess we should use the NIF-specified bounding box
|
||||
// for this, not our automatically calculated one.
|
||||
extern "C" void bullet_createBoxShape(float xmin, float ymin, float zmin,
|
||||
float xmax, float ymax, float zmax,
|
||||
float *trans, float *matrix)
|
||||
{
|
||||
// Make a copy of the vertex buffer, since we need to change it
|
||||
float *vbuffer = (float*)copyBuffer(cube_verts, 12, cube_num_verts);
|
||||
|
||||
// Calculate the widths
|
||||
float xwidth = xmax-xmin;
|
||||
float ywidth = ymax-ymin;
|
||||
float zwidth = zmax-zmin;
|
||||
|
||||
// Transform the cube to (xmin,xmax) etc
|
||||
float *vb = vbuffer;
|
||||
for(int i=0; i<cube_num_verts; i++)
|
||||
{
|
||||
*vb = (*vb)*xwidth + xmin; vb++;
|
||||
*vb = (*vb)*ywidth + ymin; vb++;
|
||||
*vb = (*vb)*zwidth + zmin; vb++;
|
||||
}
|
||||
|
||||
// Insert the trimesh
|
||||
createTriShape(cube_num_tris*3, cube_tris,
|
||||
cube_num_verts, vbuffer,
|
||||
trans, matrix);
|
||||
}
|
||||
|
||||
// Create a triangle shape and insert it into the current index/vertex
|
||||
// array. If no array is active, create one.
|
||||
extern "C" void bullet_createTriShape(int32_t numFaces,
|
||||
void *triArray,
|
||||
int32_t numVerts,
|
||||
void *vertArray,
|
||||
float *trans,
|
||||
float *matrix)
|
||||
{
|
||||
createTriShape(numFaces, copyBuffer(triArray, 2, numFaces),
|
||||
numVerts, copyBuffer(vertArray, 12, numVerts),
|
||||
trans, matrix);
|
||||
}
|
||||
|
||||
// Get the shape built up so far, if any. This clears g_currentMesh,
|
||||
// so the next call to createTriShape will start a new shape.
|
||||
extern "C" btCollisionShape *bullet_getFinalShape()
|
||||
{
|
||||
btCollisionShape *shape;
|
||||
|
||||
// Create a shape from all the inserted completed meshes
|
||||
shape = NULL;
|
||||
if(g_currentMesh != NULL)
|
||||
shape = new btBvhTriangleMeshShape(g_currentMesh, false);
|
||||
|
||||
// Clear these for the next NIF
|
||||
g_currentMesh = NULL;
|
||||
return shape;
|
||||
}
|
||||
|
||||
// Insert a static mesh
|
||||
extern "C" void bullet_insertStatic(btConcaveShape *shape,
|
||||
float *pos,
|
||||
float *quat,
|
||||
float scale)
|
||||
{
|
||||
// FIXME: Scaling does NOT work.
|
||||
|
||||
// Are we scaled?
|
||||
if(scale != 1.0)
|
||||
{
|
||||
//cout << "Scaling shape " << shape << " by " << scale << endl;
|
||||
|
||||
// Not quite sure how to handle local scaling yet. Our initial
|
||||
// attempt was to create a wrapper that showed a scale mesh to
|
||||
// the "outside world" while referencing the original, but I
|
||||
// suspect it ended up altering the original data. At least it
|
||||
// doesn't work the way it is now, and only crashes.
|
||||
|
||||
// The alternative is to create a new copy of the shape for each
|
||||
// scaled version we insert. This is wasteful, but might be
|
||||
// acceptable.
|
||||
|
||||
// It's also possible we can achieve this effect by changing
|
||||
// larger parts of the Bullet library - but I hope I don't have
|
||||
// to create my own dispatcher and such. Finally, even if the
|
||||
// transformations given to objects are supposed to be uniform
|
||||
// in length, maybe we can cheat the system and scale the
|
||||
// transformation instead. Try it just for kicks, and go through
|
||||
// the system to see what parts of Bullet it would break.
|
||||
|
||||
// In any case, when we find a solution we should apply it to
|
||||
// all shapes (not just scale!=1.0) to get a better impression
|
||||
// of any performance and memory overhead.
|
||||
|
||||
// Also, as an optimization, it looks like multiple instances of
|
||||
// the same shape are often inserted with the same scale
|
||||
// factor. We could easily cache this. The scale-recreation of
|
||||
// meshes (in necessary) could be done as a separate function,
|
||||
// and the caching could be done in D code.
|
||||
}
|
||||
|
||||
btTransform trafo;
|
||||
trafo.setIdentity();
|
||||
trafo.setOrigin(btVector3(pos[0], pos[1], pos[2]));
|
||||
|
||||
// Ogre uses WXYZ quaternions, Bullet uses XYZW.
|
||||
trafo.setRotation(btQuaternion(quat[1], quat[2], quat[3], quat[0]));
|
||||
|
||||
// Create and insert the collision object
|
||||
btCollisionObject *obj = new btCollisionObject();
|
||||
obj->setCollisionShape(shape);
|
||||
obj->setWorldTransform(trafo);
|
||||
g_dynamicsWorld->addCollisionObject(obj, MASK_STATIC, MASK_PLAYER);
|
||||
}
|
||||
|
||||
// Move the physics simulation 'delta' seconds forward in time
|
||||
extern "C" void bullet_timeStep(float delta)
|
||||
{
|
||||
TRACE("bullet_timeStep");
|
||||
// TODO: We might experiment with the number of time steps. Remember
|
||||
// that the function also returns the number of steps performed.
|
||||
g_dynamicsWorld->stepSimulation(delta,2);
|
||||
}
|
||||
|
||||
// Cleanup in the reverse order of creation/initialization
|
||||
extern "C" void bullet_cleanup()
|
||||
{
|
||||
// Remove the rigidbodies from the dynamics world and delete them
|
||||
for (int i=g_dynamicsWorld->getNumCollisionObjects()-1; i>=0 ;i--)
|
||||
{
|
||||
btCollisionObject* obj = g_dynamicsWorld->getCollisionObjectArray()[i];
|
||||
btRigidBody* body = btRigidBody::upcast(obj);
|
||||
|
||||
if (body && body->getMotionState())
|
||||
delete body->getMotionState();
|
||||
|
||||
g_dynamicsWorld->removeCollisionObject( obj );
|
||||
delete obj;
|
||||
}
|
||||
|
||||
delete g_dynamicsWorld;
|
||||
delete g_solver;
|
||||
delete g_broadphase;
|
||||
delete g_dispatcher;
|
||||
delete g_collisionConfiguration;
|
||||
}
|
@ -1,381 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
(see additional copyrights for this file below)
|
||||
|
||||
This file (cpp_player.cpp) 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/ .
|
||||
|
||||
----
|
||||
|
||||
Parts of this file is based on the kinematic character controller
|
||||
demo included with the Bullet library. The copyright statement for
|
||||
these parts follow:
|
||||
|
||||
Bullet Continuous Collision Detection and Physics Library
|
||||
Copyright (c) 2003-2006 Erwin Coumans http://continuousphysics.com/Bullet/
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any
|
||||
damages arising from the use of this software. Permission is
|
||||
granted to anyone to use this software for any purpose, including
|
||||
commercial applications, and to alter it and redistribute it freely,
|
||||
subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product
|
||||
documentation would be appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must
|
||||
not be misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
*/
|
||||
|
||||
|
||||
// This file handles player-specific physics and collision detection
|
||||
|
||||
// TODO: Later we might handle various physics modes, eg. dynamic
|
||||
// (full physics), player_walk, player_fall, player_swim,
|
||||
// player_float, player_levitate, player_ghost. These would be
|
||||
// applicable to any object (through Monster script), allowing the
|
||||
// physics code to be shared between NPCs, creatures and the player.
|
||||
|
||||
// Variables used internally in this file. Once we make per-object
|
||||
// player collision, these will be member variables.
|
||||
bool g_touchingContact;
|
||||
btVector3 g_touchingNormal;
|
||||
btScalar g_currentStepOffset;
|
||||
float g_stepHeight = 5;
|
||||
|
||||
// Returns the reflection direction of a ray going 'direction' hitting
|
||||
// a surface with normal 'normal'
|
||||
btVector3 reflect (const btVector3& direction, const btVector3& normal)
|
||||
{ return direction - (btScalar(2.0) * direction.dot(normal)) * normal; }
|
||||
|
||||
// Returns the portion of 'direction' that is perpendicular to
|
||||
// 'normal'
|
||||
btVector3 perpComponent (const btVector3& direction, const btVector3& normal)
|
||||
{ return direction - normal * direction.dot(normal); }
|
||||
|
||||
btManifoldArray manifoldArray;
|
||||
|
||||
// Callback used for collision detection sweep tests. It prevents self
|
||||
// collision and is used in calls to convexSweepTest(). TODO: It might
|
||||
// be enough to just set the filters on this. If we set the group and
|
||||
// mask so that we only collide with static objects, self collision
|
||||
// would never happen. The sweep test function should have had a
|
||||
// version where you only specify the filters - I might add that
|
||||
// myself.
|
||||
class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback
|
||||
{
|
||||
public:
|
||||
ClosestNotMeConvexResultCallback()
|
||||
: btCollisionWorld::ClosestConvexResultCallback
|
||||
(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0))
|
||||
{
|
||||
m_collisionFilterGroup = g_playerObject->
|
||||
getBroadphaseHandle()->m_collisionFilterGroup;
|
||||
|
||||
m_collisionFilterMask = g_playerObject->
|
||||
getBroadphaseHandle()->m_collisionFilterMask;
|
||||
}
|
||||
|
||||
btScalar addSingleResult(btCollisionWorld::LocalConvexResult&
|
||||
convexResult, bool normalInWorldSpace)
|
||||
{
|
||||
if (convexResult.m_hitCollisionObject == g_playerObject) return 1.0;
|
||||
|
||||
return ClosestConvexResultCallback::addSingleResult
|
||||
(convexResult, normalInWorldSpace);
|
||||
}
|
||||
};
|
||||
|
||||
// Used to step up small steps and slopes.
|
||||
void stepUp()
|
||||
{
|
||||
// phase 1: up
|
||||
btVector3 targetPosition = g_playerPosition +
|
||||
btVector3(0.0, 0.0, g_stepHeight);
|
||||
btTransform start, end;
|
||||
|
||||
start.setIdentity ();
|
||||
end.setIdentity ();
|
||||
|
||||
// FIXME: Handle penetration properly
|
||||
start.setOrigin (g_playerPosition + btVector3(0.0, 0.1, 0.0));
|
||||
end.setOrigin (targetPosition);
|
||||
|
||||
ClosestNotMeConvexResultCallback callback;
|
||||
g_dynamicsWorld->convexSweepTest (g_playerShape, start, end, callback);
|
||||
|
||||
if (callback.hasHit())
|
||||
{
|
||||
// we moved up only a fraction of the step height
|
||||
g_currentStepOffset = g_stepHeight * callback.m_closestHitFraction;
|
||||
g_playerPosition.setInterpolate3(g_playerPosition, targetPosition,
|
||||
callback.m_closestHitFraction);
|
||||
}
|
||||
else
|
||||
{
|
||||
g_currentStepOffset = g_stepHeight;
|
||||
g_playerPosition = targetPosition;
|
||||
}
|
||||
}
|
||||
|
||||
void updateTargetPositionBasedOnCollision (const btVector3& hitNormal,
|
||||
btVector3 &targetPosition)
|
||||
{
|
||||
btVector3 movementDirection = targetPosition - g_playerPosition;
|
||||
btScalar movementLength = movementDirection.length();
|
||||
|
||||
if (movementLength <= SIMD_EPSILON)
|
||||
return;
|
||||
|
||||
// Is this needed?
|
||||
movementDirection.normalize();
|
||||
|
||||
btVector3 reflectDir = reflect(movementDirection, hitNormal);
|
||||
reflectDir.normalize();
|
||||
|
||||
btVector3 perpendicularDir = perpComponent (reflectDir, hitNormal);
|
||||
|
||||
targetPosition = g_playerPosition;
|
||||
targetPosition += perpendicularDir * movementLength;
|
||||
}
|
||||
|
||||
// This covers all normal forward movement and collision, including
|
||||
// walking sideways when hitting a wall at an angle. It does NOT
|
||||
// handle walking up slopes and steps, or falling/gravity.
|
||||
void stepForward(btVector3& walkMove)
|
||||
{
|
||||
btVector3 originalDir = walkMove.normalized();
|
||||
|
||||
// If no walking direction is given, we still run the function. This
|
||||
// allows moving forces to push the player around even if she is
|
||||
// standing still.
|
||||
if (walkMove.length() < SIMD_EPSILON)
|
||||
originalDir.setValue(0.f,0.f,0.f);
|
||||
|
||||
btTransform start, end;
|
||||
btVector3 targetPosition = g_playerPosition + walkMove;
|
||||
start.setIdentity ();
|
||||
end.setIdentity ();
|
||||
|
||||
btScalar fraction = 1.0;
|
||||
btScalar distance2 = (g_playerPosition-targetPosition).length2();
|
||||
|
||||
if (g_touchingContact)
|
||||
if (originalDir.dot(g_touchingNormal) > btScalar(0.0))
|
||||
updateTargetPositionBasedOnCollision (g_touchingNormal, targetPosition);
|
||||
|
||||
int maxIter = 10;
|
||||
|
||||
while (fraction > btScalar(0.01) && maxIter-- > 0)
|
||||
{
|
||||
start.setOrigin (g_playerPosition);
|
||||
end.setOrigin (targetPosition);
|
||||
|
||||
ClosestNotMeConvexResultCallback callback;
|
||||
g_dynamicsWorld->convexSweepTest (g_playerShape, start, end, callback);
|
||||
|
||||
fraction -= callback.m_closestHitFraction;
|
||||
|
||||
if (callback.hasHit())
|
||||
{
|
||||
// We moved only a fraction
|
||||
btScalar hitDistance = (callback.m_hitPointWorld - g_playerPosition).length();
|
||||
// If the distance is further than the collision margin,
|
||||
// move
|
||||
if (hitDistance > 0.05)
|
||||
g_playerPosition.setInterpolate3(g_playerPosition, targetPosition,
|
||||
callback.m_closestHitFraction);
|
||||
|
||||
updateTargetPositionBasedOnCollision(callback.m_hitNormalWorld,
|
||||
targetPosition);
|
||||
btVector3 currentDir = targetPosition - g_playerPosition;
|
||||
distance2 = currentDir.length2();
|
||||
|
||||
if (distance2 <= SIMD_EPSILON)
|
||||
break;
|
||||
|
||||
currentDir.normalize();
|
||||
|
||||
if (currentDir.dot(originalDir) <= btScalar(0.0))
|
||||
break;
|
||||
}
|
||||
else
|
||||
// we moved the whole way
|
||||
g_playerPosition = targetPosition;
|
||||
}
|
||||
}
|
||||
|
||||
void stepDown (btScalar dt)
|
||||
{
|
||||
btTransform start, end;
|
||||
|
||||
// phase 3: down
|
||||
btVector3 step_drop = btVector3(0,0,g_currentStepOffset);
|
||||
btVector3 gravity_drop = btVector3(0,0,g_stepHeight);
|
||||
|
||||
btVector3 targetPosition = g_playerPosition - step_drop - gravity_drop;
|
||||
|
||||
start.setIdentity ();
|
||||
end.setIdentity ();
|
||||
|
||||
start.setOrigin (g_playerPosition);
|
||||
end.setOrigin (targetPosition);
|
||||
|
||||
ClosestNotMeConvexResultCallback callback;
|
||||
g_dynamicsWorld->convexSweepTest(g_playerShape, start, end, callback);
|
||||
|
||||
if (callback.hasHit())
|
||||
// we dropped a fraction of the height -> hit floor
|
||||
g_playerPosition.setInterpolate3(g_playerPosition, targetPosition,
|
||||
callback.m_closestHitFraction);
|
||||
else
|
||||
// we dropped the full height
|
||||
g_playerPosition = targetPosition;
|
||||
}
|
||||
|
||||
// Check if the player currently collides with anything, and adjust
|
||||
// its position accordingly. Returns true if collisions were found.
|
||||
bool recoverFromPenetration()
|
||||
{
|
||||
bool penetration = false;
|
||||
|
||||
// Update the collision pair cache
|
||||
g_dispatcher->dispatchAllCollisionPairs(g_pairCache,
|
||||
g_dynamicsWorld->getDispatchInfo(),
|
||||
g_dispatcher);
|
||||
|
||||
btScalar maxPen = 0.0;
|
||||
for (int i = 0; i < g_pairCache->getNumOverlappingPairs(); i++)
|
||||
{
|
||||
manifoldArray.resize(0);
|
||||
|
||||
btBroadphasePair* collisionPair = &g_pairCache->getOverlappingPairArray()[i];
|
||||
// Get the contact points
|
||||
if (collisionPair->m_algorithm)
|
||||
collisionPair->m_algorithm->getAllContactManifolds(manifoldArray);
|
||||
|
||||
// And handle them
|
||||
for (int j=0;j<manifoldArray.size();j++)
|
||||
{
|
||||
btPersistentManifold* manifold = manifoldArray[j];
|
||||
btScalar directionSign = manifold->getBody0() ==
|
||||
g_playerObject ? btScalar(-1.0) : btScalar(1.0);
|
||||
|
||||
for (int p=0;p<manifold->getNumContacts();p++)
|
||||
{
|
||||
const btManifoldPoint &pt = manifold->getContactPoint(p);
|
||||
|
||||
if (pt.getDistance() < 0.0)
|
||||
{
|
||||
// Pick out the maximum penetration normal and store
|
||||
// it
|
||||
if (pt.getDistance() < maxPen)
|
||||
{
|
||||
maxPen = pt.getDistance();
|
||||
g_touchingNormal = pt.m_normalWorldOnB * directionSign;//??
|
||||
|
||||
}
|
||||
g_playerPosition += pt.m_normalWorldOnB * directionSign *
|
||||
pt.getDistance() * btScalar(0.2);
|
||||
|
||||
penetration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
btTransform newTrans = g_playerObject->getWorldTransform();
|
||||
newTrans.setOrigin(g_playerPosition);
|
||||
g_playerObject->setWorldTransform(newTrans);
|
||||
|
||||
return penetration;
|
||||
}
|
||||
|
||||
// Callback called at the end of each simulation cycle. This is the
|
||||
// main function is responsible for player movement.
|
||||
void playerStepCallback(btDynamicsWorld* dynamicsWorld, btScalar timeStep)
|
||||
{
|
||||
// The walking direction is set from D code each frame, and the
|
||||
// final player position is read back from D code after the
|
||||
// simulation.
|
||||
btVector3 walkStep = g_walkDirection * timeStep;
|
||||
|
||||
float len = walkStep.length();
|
||||
|
||||
// In walk mode, it shouldn't matter whether or not we look up or
|
||||
// down. Rotate the vector back to the horizontal plane.
|
||||
if(g_physMode == PHYS_WALK)
|
||||
{
|
||||
walkStep.setZ(0);
|
||||
float len2 = walkStep.length();
|
||||
if(len2 > 0)
|
||||
walkStep *= len/len2;
|
||||
}
|
||||
|
||||
// Get the player position
|
||||
g_playerPosition = g_playerObject->getWorldTransform().getOrigin();
|
||||
|
||||
if(g_physMode == PHYS_GHOST)
|
||||
{
|
||||
// Ghost mode - just move, no collision
|
||||
g_playerPosition += walkStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Collision detection is active
|
||||
|
||||
// Before moving, recover from current penetrations
|
||||
int numPenetrationLoops = 0;
|
||||
g_touchingContact = false;
|
||||
while (recoverFromPenetration())
|
||||
{
|
||||
numPenetrationLoops++;
|
||||
g_touchingContact = true;
|
||||
|
||||
// Make sure we don't stay here indefinitely
|
||||
if (numPenetrationLoops > 4)
|
||||
break;
|
||||
}
|
||||
|
||||
// recoverFromPenetration updates g_playerPosition and the
|
||||
// collision mesh, so they are still in sync at this point
|
||||
|
||||
// Next, do the walk. The following functions only updates
|
||||
// g_playerPosition, they do not move the collision object.
|
||||
|
||||
if(g_physMode == PHYS_WALK)
|
||||
{
|
||||
stepUp();
|
||||
stepForward(walkStep);
|
||||
stepDown(timeStep);
|
||||
}
|
||||
else if(g_physMode == PHYS_FLY)
|
||||
stepForward(walkStep);
|
||||
else
|
||||
cout << "WARNING: Unknown physics mode " << g_physMode << "!\n";
|
||||
}
|
||||
|
||||
// Move the player collision mesh
|
||||
btTransform xform = g_playerObject->getWorldTransform ();
|
||||
xform.setOrigin (g_playerPosition);
|
||||
g_playerObject->setWorldTransform (xform);
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (cpp_scale.cpp) 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/ .
|
||||
|
||||
*/
|
||||
|
||||
// WARNING: This file does NOT work, and it is not used yet.
|
||||
|
||||
class ScaleCallback : public btTriangleCallback
|
||||
{
|
||||
btTriangleCallback *call;
|
||||
float factor;
|
||||
|
||||
public:
|
||||
ScaleCallback(btTriangleCallback *c, float f)
|
||||
{ call = c; factor = f; }
|
||||
|
||||
void processTriangle(btVector3 *tri, int partid, int triindex)
|
||||
{
|
||||
btVector3 vecs[3];
|
||||
vecs[0] = tri[0]*factor;
|
||||
vecs[1] = tri[1]*factor;
|
||||
vecs[2] = tri[2]*factor;
|
||||
|
||||
call->processTriangle(vecs, partid, triindex);
|
||||
}
|
||||
};
|
||||
|
||||
// This class is used to uniformly scale a triangle mesh by a
|
||||
// factor. It wraps around an existing shape and does not copy the
|
||||
// data.
|
||||
class ScaleShape : public btConcaveShape
|
||||
{
|
||||
btConcaveShape* child;
|
||||
float factor, fact3, facthalf;
|
||||
|
||||
public:
|
||||
|
||||
ScaleShape(btConcaveShape* ch, float ft)
|
||||
{
|
||||
child = ch;
|
||||
factor = ft;
|
||||
fact3 = factor*factor*factor;
|
||||
facthalf = factor*0.5;
|
||||
}
|
||||
|
||||
void calculateLocalInertia(btScalar mass,btVector3& inertia) const
|
||||
{
|
||||
btVector3 tmpInertia;
|
||||
child->calculateLocalInertia(mass,tmpInertia);
|
||||
inertia = tmpInertia * fact3;
|
||||
}
|
||||
|
||||
const char* getName()const { return "ScaleShape"; }
|
||||
|
||||
void getAabb(const btTransform& t,btVector3& aabbMin,btVector3& aabbMax) const
|
||||
{
|
||||
child->getAabb(t,aabbMin,aabbMax);
|
||||
btVector3 aabbCenter = (aabbMax+aabbMin)*0.5;
|
||||
btVector3 scaledAabbHalfExtends = (aabbMax-aabbMin)*facthalf;
|
||||
|
||||
aabbMin = aabbCenter - scaledAabbHalfExtends;
|
||||
aabbMax = aabbCenter + scaledAabbHalfExtends;
|
||||
}
|
||||
|
||||
void processAllTriangles(btTriangleCallback *callback,const btVector3& aabbMin,const btVector3& aabbMax) const
|
||||
{
|
||||
ScaleCallback scb(callback, factor);
|
||||
|
||||
child->processAllTriangles(&scb, aabbMin, aabbMax);
|
||||
}
|
||||
|
||||
void setLocalScaling(const btVector3& scaling)
|
||||
{ child->setLocalScaling(scaling); }
|
||||
|
||||
const btVector3& getLocalScaling() const
|
||||
{ return child->getLocalScaling(); }
|
||||
|
||||
int getShapeType() const
|
||||
{ return TRIANGLE_MESH_SHAPE_PROXYTYPE; }
|
||||
};
|
@ -1,437 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (config.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 core.config;
|
||||
|
||||
import std.string;
|
||||
import std.file;
|
||||
import std.path;
|
||||
import std.stdio;
|
||||
|
||||
import monster.monster;
|
||||
import monster.util.string;
|
||||
|
||||
import core.inifile;
|
||||
import core.filefinder;
|
||||
|
||||
import sound.audio;
|
||||
|
||||
import input.keys;
|
||||
import input.ois;
|
||||
|
||||
import ogre.ogre;
|
||||
|
||||
ConfigManager config;
|
||||
|
||||
/*
|
||||
* Structure that handles all user adjustable configuration options,
|
||||
* including things like file paths, plugins, graphics resolution,
|
||||
* game settings, window positions, etc. It is also responsible for
|
||||
* reading and writing configuration files, for importing settings
|
||||
* from Morrowind.ini and for configuring OGRE. It doesn't currently
|
||||
* DO all of this, but it is supposed to in the future.
|
||||
*/
|
||||
|
||||
struct ConfigManager
|
||||
{
|
||||
MonsterObject *mo;
|
||||
|
||||
IniWriter iniWriter;
|
||||
|
||||
// Mouse sensitivity
|
||||
float *mouseSensX;
|
||||
float *mouseSensY;
|
||||
bool *flipMouseY;
|
||||
|
||||
// Ogre configuration
|
||||
bool showOgreConfig; // The configuration setting
|
||||
// The actual result, overridable by a command line switch, and also
|
||||
// set to true if firstRun is true.
|
||||
bool finalOgreConfig;
|
||||
|
||||
// Other settings
|
||||
bool firstRun;
|
||||
|
||||
// Set to true if sound is completely disabled
|
||||
bool noSound = false;
|
||||
|
||||
// Number of current screen shot. Saved upon exit, so that shots
|
||||
// from separate sessions don't overwrite each other.
|
||||
int screenShotNum;
|
||||
|
||||
// Game files to load (max 255)
|
||||
char[][] gameFiles;
|
||||
|
||||
// Directories
|
||||
char[] dataDir;
|
||||
char[] esmDir;
|
||||
char[] bsaDir;
|
||||
char[] sndDir;
|
||||
char[] fontDir;
|
||||
char[] musDir; // Explore music
|
||||
char[] musDir2; // Battle music
|
||||
|
||||
// Configuration file
|
||||
char[] confFile = "openmw.ini";
|
||||
|
||||
// Cell to load at startup
|
||||
char[] defaultCell;
|
||||
|
||||
// These set the volume to a new value and updates all sounds to
|
||||
// take notice.
|
||||
void setMusicVolume(float vol)
|
||||
{
|
||||
stack.pushFloat(vol);
|
||||
mo.call("setMusicVolume");
|
||||
}
|
||||
float getMusicVolume()
|
||||
{ return mo.getFloat("musicVolume"); }
|
||||
|
||||
void setSfxVolume(float vol)
|
||||
{
|
||||
stack.pushFloat(vol);
|
||||
mo.call("setSfxVolume");
|
||||
}
|
||||
float getSfxVolume()
|
||||
{ return mo.getFloat("sfxVolume"); }
|
||||
|
||||
void setMainVolume(float vol)
|
||||
{
|
||||
stack.pushFloat(vol);
|
||||
mo.call("setMainVolume");
|
||||
}
|
||||
float getMainVolume()
|
||||
{ return mo.getFloat("mainVolume"); }
|
||||
|
||||
// Initialize the config manager. Send a 'true' parameter to reset
|
||||
// all keybindings to the default. A lot of this stuff will be moved
|
||||
// to script code at some point. In general, all input mechanics and
|
||||
// distribution of key events should happen in native code, while
|
||||
// all setup and control should be handled in script code.
|
||||
void initialize(bool reset = false)
|
||||
{
|
||||
// Initialize variables from Monster.
|
||||
assert(mo !is null);
|
||||
mouseSensX = mo.getFloatPtr("mouseSensX");
|
||||
mouseSensY = mo.getFloatPtr("mouseSensY");
|
||||
flipMouseY = mo.getBoolPtr("flipMouseY");
|
||||
|
||||
// Initialize the key binding manager
|
||||
keyBindings.initKeys();
|
||||
|
||||
/* Disable this at the moment. It's a good idea to put
|
||||
configuration in a central location, but it's useless as long
|
||||
as Ogre expects to find it's files in the current working
|
||||
directory. The best permanent solution would be to let the
|
||||
locations of ogre.cfg and plugins.cfg be determined by
|
||||
openmw.ini - I will fix that later.
|
||||
|
||||
version(Posix)
|
||||
{
|
||||
if(!exists(confFile))
|
||||
confFile = expandTilde("~/.openmw/openmw.ini");
|
||||
}
|
||||
*/
|
||||
|
||||
readIni(reset);
|
||||
}
|
||||
|
||||
// Read config from morro.ini, if it exists. The reset parameter is
|
||||
// set to true if we should use default key bindings instead of the
|
||||
// ones from the config file.
|
||||
void readIni(bool reset)
|
||||
{
|
||||
// Read configuration file, if it exists.
|
||||
IniReader ini;
|
||||
|
||||
ini.readFile(confFile);
|
||||
|
||||
screenShotNum = ini.getInt("General", "Screenshots", 0);
|
||||
float mainVolume = saneVol(ini.getFloat("Sound", "Main Volume", 0.7));
|
||||
float musicVolume = saneVol(ini.getFloat("Sound", "Music Volume", 0.5));
|
||||
float sfxVolume = saneVol(ini.getFloat("Sound", "SFX Volume", 0.5));
|
||||
bool useMusic = ini.getBool("Sound", "Enable Music", true);
|
||||
|
||||
|
||||
lightConst = ini.getInt("LightAttenuation", "UseConstant", 0);
|
||||
lightConstValue = ini.getFloat("LightAttenuation", "ConstantValue", 0.0);
|
||||
|
||||
lightLinear = ini.getInt("LightAttenuation", "UseLinear", 1);
|
||||
lightLinearMethod = ini.getInt("LightAttenuation", "LinearMethod", 1);
|
||||
lightLinearValue = ini.getFloat("LightAttenuation", "LinearValue", 3.0);
|
||||
lightLinearRadiusMult = ini.getFloat("LightAttenuation", "LinearRadiusMult", 1.0);
|
||||
|
||||
lightQuadratic = ini.getInt("LightAttenuation", "UseQuadratic", 0);
|
||||
lightQuadraticMethod = ini.getInt("LightAttenuation", "QuadraticMethod", 2);
|
||||
lightQuadraticValue = ini.getFloat("LightAttenuation", "QuadraticValue", 16.0);
|
||||
lightQuadraticRadiusMult = ini.getFloat("LightAttenuation", "QuadraticRadiusMult", 1.0);
|
||||
|
||||
lightOutQuadInLin = ini.getInt("LightAttenuation", "OutQuadInLin", 0);
|
||||
|
||||
|
||||
*mouseSensX = ini.getFloat("Controls", "Mouse Sensitivity X", 0.2);
|
||||
*mouseSensY = ini.getFloat("Controls", "Mouse Sensitivity Y", 0.2);
|
||||
*flipMouseY = ini.getBool("Controls", "Flip Mouse Y Axis", false);
|
||||
|
||||
mo.setFloat("mainVolume", mainVolume);
|
||||
mo.setFloat("musicVolume", musicVolume);
|
||||
mo.setFloat("sfxVolume", sfxVolume);
|
||||
mo.setBool("useMusic", useMusic);
|
||||
|
||||
defaultCell = ini.getString("General", "Default Cell", "Assu");
|
||||
|
||||
firstRun = ini.getBool("General", "First Run", true);
|
||||
showOgreConfig = ini.getBool("General", "Show Ogre Config", false);
|
||||
|
||||
// This flag determines whether we will actually show the Ogre
|
||||
// config dialogue. The EITHER of the following are true, the
|
||||
// config box will be shown:
|
||||
// - The program is being run for the first time
|
||||
// - The "Show Ogre Config" option in openmw.ini is set.
|
||||
// - The -oc option is specified on the command line
|
||||
// - The file ogre.cfg is missing
|
||||
|
||||
finalOgreConfig = showOgreConfig || firstRun ||
|
||||
!exists("ogre.cfg");
|
||||
|
||||
// Set default key bindings first.
|
||||
with(keyBindings)
|
||||
{
|
||||
// Bind some default keys
|
||||
bind(Keys.MoveLeft, KC.A, KC.LEFT);
|
||||
bind(Keys.MoveRight, KC.D, KC.RIGHT);
|
||||
bind(Keys.MoveForward, KC.W, KC.UP);
|
||||
bind(Keys.MoveBackward, KC.S, KC.DOWN);
|
||||
bind(Keys.MoveUp, KC.LSHIFT);
|
||||
bind(Keys.MoveDown, KC.LCONTROL);
|
||||
|
||||
bind(Keys.MainVolUp, KC.ADD);
|
||||
bind(Keys.MainVolDown, KC.SUBTRACT);
|
||||
bind(Keys.MusVolDown, KC.N1);
|
||||
bind(Keys.MusVolUp, KC.N2);
|
||||
bind(Keys.SfxVolDown, KC.N3);
|
||||
bind(Keys.SfxVolUp, KC.N4);
|
||||
bind(Keys.Mute, KC.M);
|
||||
|
||||
bind(Keys.Fullscreen, KC.F);
|
||||
|
||||
bind(Keys.ToggleBattleMusic, KC.SPACE);
|
||||
bind(Keys.PhysMode, KC.T);
|
||||
bind(Keys.Nighteye, KC.N);
|
||||
bind(Keys.ToggleGui, KC.Mouse1);
|
||||
bind(Keys.Console, KC.F1, KC.GRAVE);
|
||||
bind(Keys.Debug, KC.G);
|
||||
|
||||
bind(Keys.Pause, KC.PAUSE, KC.P);
|
||||
bind(Keys.ScreenShot, KC.SYSRQ);
|
||||
bind(Keys.Exit, KC.Q, KC.ESCAPE);
|
||||
}
|
||||
|
||||
// Unless the ini file was missing or we were asked to reset all
|
||||
// keybindings to default, replace all present bindings with the
|
||||
// values from the ini.
|
||||
if(!reset && ini.wasRead)
|
||||
{
|
||||
// Read key bindings
|
||||
for(int i; i<Keys.Length; i++)
|
||||
{
|
||||
char[] s = keyToString[i];
|
||||
if(s.length)
|
||||
{
|
||||
char[] iniVal = ini.getString("Bindings", s, "_def");
|
||||
|
||||
// Was the setting present in the ini file?
|
||||
if(iniVal != "_def")
|
||||
// If so, bind it!
|
||||
keyBindings.bindComma(cast(Keys)i, iniVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read data file directory
|
||||
dataDir = ini.getString("General", "Data Directory", "data/");
|
||||
|
||||
// Make sure there's a trailing slash at the end. The forward slash
|
||||
// / works on all platforms, while the backslash \ does not. This
|
||||
// isn't super robust, but we will fix a general path handle
|
||||
// mechanism later (or use an existing one.)
|
||||
if(dataDir.ends("\\")) dataDir[$-1] = '/';
|
||||
if(!dataDir.ends("/")) dataDir ~= '/';
|
||||
|
||||
bsaDir = dataDir;
|
||||
esmDir = dataDir;
|
||||
sndDir = dataDir ~ "Sound/";
|
||||
fontDir = dataDir ~ "Fonts/";
|
||||
musDir = dataDir ~ "Music/Explore/";
|
||||
musDir2 = dataDir ~ "Music/Battle/";
|
||||
|
||||
// A maximum of 255 game files are allowed. Search the whole range
|
||||
// in case some holes developed in the number sequence. This isn't
|
||||
// a great way of specifying files (it's just a copy of the flawed
|
||||
// model that Morrowind uses), but it will do for the time being.
|
||||
FileFinder srch = new FileFinder(esmDir, null, Recurse.No);
|
||||
for(int i = 0;i < 255;i++)
|
||||
{
|
||||
char[] s = ini.getString("Game Files", format("GameFile[%d]",i), null);
|
||||
if(s != null && srch.has(s))
|
||||
gameFiles ~= esmDir ~ s;
|
||||
}
|
||||
delete srch;
|
||||
|
||||
if(gameFiles.length == 0)
|
||||
{
|
||||
// No game files set. Look in the esmDir for Morrowind.esm.
|
||||
// We can add Tribunal.esm, and Bloodmoon.esm as defaults too
|
||||
// later, when we're out of testing mode.
|
||||
char[][] baseFiles = ["Morrowind.esm"];
|
||||
//char[][] baseFiles = ["Morrowind.esm","Tribunal.esm","Bloodmoon.esm"];
|
||||
srch = new FileFinder(esmDir, "esm", Recurse.No);
|
||||
|
||||
foreach(ref s; baseFiles)
|
||||
{
|
||||
if(srch.has(s))
|
||||
{
|
||||
writefln("Adding game file %s", s);
|
||||
gameFiles ~= esmDir ~ s;
|
||||
}
|
||||
}
|
||||
delete srch;
|
||||
}
|
||||
|
||||
// FIXME: Must sort gameFiles so that ESMs come first, then ESPs.
|
||||
// I don't know if this needs to be done by filename, or by the
|
||||
// actual file type..
|
||||
// Further sort the two groups by file date (oldest first).
|
||||
|
||||
/* Don't bother reading every directory seperately
|
||||
bsaDir = ini.getString("General", "BSA Directory", "data/");
|
||||
esmDir = ini.getString("General", "ESM Directory", "data/");
|
||||
sndDir = ini.getString("General", "SFX Directory", "data/Sound/");
|
||||
musDir = ini.getString("General", "Explore Music Directory", "data/Music/Explore/");
|
||||
musDir2 = ini.getString("General", "Battle Music Directory", "data/Music/Battle/");
|
||||
*/
|
||||
}
|
||||
|
||||
// Create the config file
|
||||
void writeConfig()
|
||||
{
|
||||
//writefln("writeConfig(%s)", confFile);
|
||||
with(iniWriter)
|
||||
{
|
||||
openFile(confFile);
|
||||
|
||||
comment("Don't write your own comments in this file, they");
|
||||
comment("will disappear when the file is rewritten.");
|
||||
section("General");
|
||||
writeString("Data Directory", dataDir);
|
||||
/*
|
||||
writeString("ESM Directory", esmDir);
|
||||
writeString("BSA Directory", bsaDir);
|
||||
writeString("SFX Directory", sndDir);
|
||||
writeString("Explore Music Directory", musDir);
|
||||
writeString("Battle Music Directory", musDir2);
|
||||
*/
|
||||
writeInt("Screenshots", screenShotNum);
|
||||
writeString("Default Cell", defaultCell);
|
||||
|
||||
// Save the setting as it appeared in the input. The setting
|
||||
// you specify in the ini is persistent, specifying the -oc
|
||||
// parameter does not change it.
|
||||
writeBool("Show Ogre Config", showOgreConfig);
|
||||
|
||||
// The next run is never the first run.
|
||||
writeBool("First Run", false);
|
||||
|
||||
section("Controls");
|
||||
writeFloat("Mouse Sensitivity X", *mouseSensX);
|
||||
writeFloat("Mouse Sensitivity Y", *mouseSensY);
|
||||
writeBool("Flip Mouse Y Axis", *flipMouseY);
|
||||
|
||||
section("Bindings");
|
||||
comment("Key bindings. The strings must match exactly.");
|
||||
foreach(int i, KeyBind b; keyBindings.bindings)
|
||||
{
|
||||
char[] s = keyToString[i];
|
||||
if(s.length)
|
||||
writeString(s, b.getString());
|
||||
}
|
||||
|
||||
section("Sound");
|
||||
writeFloat("Main Volume", mo.getFloat("mainVolume"));
|
||||
writeFloat("Music Volume", mo.getFloat("musicVolume"));
|
||||
writeFloat("SFX Volume", mo.getFloat("sfxVolume"));
|
||||
writeBool("Enable Music", mo.getBool("useMusic"));
|
||||
|
||||
section("LightAttenuation");
|
||||
comment("For constant attenuation");
|
||||
writeInt("UseConstant", lightConst);
|
||||
writeFloat("ConstantValue", lightConstValue);
|
||||
comment("For linear attenuation");
|
||||
writeInt("UseLinear", lightLinear);
|
||||
writeInt("LinearMethod", lightLinearMethod);
|
||||
writeFloat("LinearValue", lightLinearValue);
|
||||
writeFloat("LinearRadiusMult", lightLinearRadiusMult);
|
||||
comment("For quadratic attenuation");
|
||||
writeInt("UseQuadratic", lightQuadratic);
|
||||
writeInt("QuadraticMethod", lightQuadraticMethod);
|
||||
writeFloat("QuadraticValue", lightQuadraticValue);
|
||||
writeFloat("QuadraticRadiusMult", lightQuadraticRadiusMult);
|
||||
comment("For quadratic in exteriors and linear in interiors");
|
||||
writeInt("OutQuadInLin", lightOutQuadInLin);
|
||||
|
||||
section("Game Files");
|
||||
foreach(int i, ref s; gameFiles)
|
||||
writeString(format("GameFile[%d]",i), s[esmDir.length..$]);
|
||||
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// In the future this will import settings from Morrowind.ini, as
|
||||
// far as this is sensible.
|
||||
void importIni()
|
||||
{
|
||||
/*
|
||||
IniReader ini;
|
||||
ini.readFile("../Morrowind.ini");
|
||||
|
||||
// Example of sensible options to convert:
|
||||
|
||||
tryArchiveFirst = ini.getInt("General", "TryArchiveFirst");
|
||||
useAudio = ( ini.getInt("General", "Disable Audio") == 0 );
|
||||
footStepVolume = ini.getFloat("General", "PC Footstep Volume");
|
||||
subtitles = ini.getInt("General", "Subtitles") == 1;
|
||||
|
||||
The plugin list (all esm and esp files) would be handled a bit
|
||||
differently. In our system they might be a per-user (per
|
||||
"character") setting, or even per-savegame. It should be safe and
|
||||
intuitive to try out a new mod without risking your savegame data
|
||||
or original settings. So these would be handled in a separate
|
||||
plugin manager.
|
||||
|
||||
In any case, the import should be interactive and user-driven, so
|
||||
there is no use in making it before we have a gui of some sort up
|
||||
and running.
|
||||
*/
|
||||
}
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (filefinder.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 core.filefinder;
|
||||
|
||||
import std.file;
|
||||
import std.string;
|
||||
|
||||
import monster.util.string;
|
||||
import monster.util.aa;
|
||||
|
||||
import core.memory;
|
||||
|
||||
import std.stdio;
|
||||
|
||||
class FileFinderException : Exception
|
||||
{
|
||||
this(char[] msg, char[] ext, char[] dir)
|
||||
{
|
||||
if(ext.length) super(format("FileFinder for %s files in %s: %s", ext, dir, msg));
|
||||
else super(format("FileFinder for %s: %s", dir, msg));
|
||||
}
|
||||
}
|
||||
|
||||
// Do we traverse directories recursively? Default is yes.
|
||||
enum Recurse { Yes, No }
|
||||
|
||||
// The file finder is used to list all files in a directory so we can
|
||||
// look up files without searching the filesystem each time. It is
|
||||
// case insensitive on all platforms, and transparently converts to
|
||||
// the right directory separator character (\ or /). We might extend
|
||||
// it later with code from other projects.
|
||||
class FileFinder
|
||||
{
|
||||
private:
|
||||
char[][] files; // Use GC for this, it's not too big and we don't
|
||||
// have to manage roots pointing to the filenames.
|
||||
HashTable!(char[], int, ESMRegionAlloc, FilenameHasher) lookup;
|
||||
|
||||
char[] dir; // Base directory to search
|
||||
char[] ext; // Extensions to pick out
|
||||
|
||||
void fail(char[] err)
|
||||
{
|
||||
throw new FileFinderException(err, ext, dir);
|
||||
}
|
||||
|
||||
// Removes the part of a path that is stored in 'dir'
|
||||
char[] removeDir(char[] path)
|
||||
{
|
||||
//TODO: Should this be case insensitive?
|
||||
assert(path[0..dir.length] == dir);
|
||||
|
||||
return path[dir.length..$];
|
||||
}
|
||||
|
||||
void insert(char[] filename)
|
||||
{
|
||||
// Only keep the part of the filename not given in 'dir'.
|
||||
char[] name = removeDir(filename);
|
||||
|
||||
if(!name.iEnds(ext)) return;
|
||||
|
||||
// We start counting from 1
|
||||
uint newVal = files.length+1;
|
||||
|
||||
// Insert it, or get the old value if it already exists
|
||||
uint oldVal = lookup[name, newVal];
|
||||
if(oldVal != newVal)
|
||||
fail("Already have " ~ name ~ "\nPreviously inserted as " ~ files[oldVal-1]);
|
||||
|
||||
// Store it
|
||||
files ~= filename;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
static char[] addSlash(char[] dir)
|
||||
{
|
||||
// Add a trailing slash
|
||||
version(Windows) if(!dir.ends("\\")) dir ~= '\\';
|
||||
version(Posix) if(!dir.ends("/")) dir ~= '/';
|
||||
return dir;
|
||||
}
|
||||
|
||||
int length() { return lookup.length; }
|
||||
|
||||
this(char[] dir, char[] ext = null, Recurse r = Recurse.Yes)
|
||||
in
|
||||
{
|
||||
if(!dir.length) fail("'dir' can not be empty");
|
||||
}
|
||||
out
|
||||
{
|
||||
assert(files.length == lookup.length);
|
||||
}
|
||||
body
|
||||
{
|
||||
// Add a trailing slash
|
||||
dir = addSlash(dir);
|
||||
|
||||
this.dir = dir;
|
||||
|
||||
if(ext.length && ext[0] != '.') ext = "." ~ ext;
|
||||
this.ext = ext;
|
||||
|
||||
bool callback(DirEntry* de)
|
||||
{
|
||||
if (de.isdir)
|
||||
{
|
||||
if(r == Recurse.Yes)
|
||||
listdir(de.name, &callback);
|
||||
}
|
||||
else
|
||||
insert(de.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
try listdir(dir, &callback);
|
||||
catch(FileException e)
|
||||
fail(e.toString);
|
||||
}
|
||||
|
||||
char[] opIndex(int i) { return files[i-1]; }
|
||||
|
||||
int opIndex(char[] file)
|
||||
{
|
||||
int i;
|
||||
|
||||
// Get value if it exists
|
||||
if(lookup.inList(file, i))
|
||||
return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool has(char[] file)
|
||||
{
|
||||
return lookup.inList(file);
|
||||
}
|
||||
|
||||
int opApply(int delegate(ref char[]) del)
|
||||
{
|
||||
int res = 0;
|
||||
|
||||
foreach(char[] s; files)
|
||||
{
|
||||
char[] tmp = removeDir(s);
|
||||
res = del(tmp);
|
||||
if(res) break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
char[] toString()
|
||||
{
|
||||
char[] result;
|
||||
foreach(char[] s; this)
|
||||
result ~= s ~ "\n";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash functions that does not differentiate between linux and
|
||||
// windows file names. This means that it is case insensitive, and
|
||||
// treats '\' and '/' as the same character. Only needed in linux, in
|
||||
// windows just use CITextHasher.
|
||||
version(Posix)
|
||||
struct FilenameHasher
|
||||
{
|
||||
static const char conv = 'a'-'A';
|
||||
|
||||
static int isEqual(char[] aa, char[] bb)
|
||||
{
|
||||
if(aa.length != bb.length) return 0;
|
||||
|
||||
foreach(int i, char a; aa)
|
||||
{
|
||||
char b = bb[i];
|
||||
|
||||
if(a == b)
|
||||
continue;
|
||||
|
||||
// Convert both to lowercase and "/ case"
|
||||
if(a <= 'Z' && a >= 'A') a += conv;
|
||||
else if(a == '\\') a = '/';
|
||||
if(b <= 'Z' && b >= 'A') b += conv;
|
||||
else if(b == '\\') b = '/';
|
||||
|
||||
if(a != b) return 0;
|
||||
}
|
||||
|
||||
// No differences were found
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint hash(char[] s)
|
||||
{
|
||||
uint hash;
|
||||
foreach (char c; s)
|
||||
{
|
||||
if(c <= 'Z' && c >= 'A') c += conv;
|
||||
else if(c == '\\') c = '/';
|
||||
hash = (hash * 37) + c;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
version(Windows) alias CITextHash FilenameHasher;
|
@ -1,187 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (defs.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 esm.defs;
|
||||
|
||||
public import std.string;
|
||||
public import monster.util.string;
|
||||
import monster.monster;
|
||||
|
||||
/*
|
||||
* Types and definitions related to parsing esm and esp files
|
||||
*/
|
||||
|
||||
alias char[4] NAME;
|
||||
alias char[32] NAME32;
|
||||
alias char[256] NAME256;
|
||||
|
||||
union Color
|
||||
{
|
||||
align(1) struct
|
||||
{
|
||||
ubyte red, green, blue, alpha;
|
||||
}
|
||||
|
||||
ubyte[4] array;
|
||||
uint value;
|
||||
|
||||
char[] toString() { return format("RGBA:%s", array); }
|
||||
}
|
||||
static assert(Color.sizeof==4);
|
||||
|
||||
// State of a record struct
|
||||
enum LoadState
|
||||
{
|
||||
Unloaded, // This record is not loaded, it has just been
|
||||
// referenced.
|
||||
Loaded, // This record has been loaded by the current file
|
||||
Previous // The record has been loaded by a previous file
|
||||
|
||||
// Finalized - might be the case for some record types, but I
|
||||
// don't know if this actual state value would be used for
|
||||
// anything.
|
||||
}
|
||||
|
||||
enum VarType { Unknown, None, Short, Int, Long, Float, String, Ignored }
|
||||
|
||||
enum SpellSchool : int
|
||||
{
|
||||
Alteration = 0,
|
||||
Conjuration = 1,
|
||||
Destruction = 2,
|
||||
Illusion = 3,
|
||||
Mysticism = 4,
|
||||
Restoration = 5,
|
||||
Length
|
||||
}
|
||||
|
||||
enum Attribute : int
|
||||
{
|
||||
Strength = 0,
|
||||
Intelligence = 1,
|
||||
Willpower = 2,
|
||||
Agility = 3,
|
||||
Speed = 4,
|
||||
Endurance = 5,
|
||||
Personality = 6,
|
||||
Luck = 7,
|
||||
Length
|
||||
}
|
||||
|
||||
enum SkillEnum : int
|
||||
{
|
||||
Block = 0,
|
||||
Armorer = 1,
|
||||
MediumArmor = 2,
|
||||
HeavyArmor = 3,
|
||||
BluntWeapon = 4,
|
||||
LongBlade = 5,
|
||||
Axe = 6,
|
||||
Spear = 7,
|
||||
Athletics = 8,
|
||||
Enchant = 9,
|
||||
Destruction = 10,
|
||||
Alteration = 11,
|
||||
Illusion = 12,
|
||||
Conjuration = 13,
|
||||
Mysticism = 14,
|
||||
Restoration = 15,
|
||||
Alchemy = 16,
|
||||
Unarmored = 17,
|
||||
Security = 18,
|
||||
Sneak = 19,
|
||||
Acrobatics = 20,
|
||||
LightArmor = 21,
|
||||
ShortBlade = 22,
|
||||
Marksman = 23,
|
||||
Mercantile = 24,
|
||||
Speechcraft = 25,
|
||||
HandToHand = 26,
|
||||
Length
|
||||
}
|
||||
|
||||
// Shared between SPEL (Spells), ALCH (Potions) and ENCH (Item
|
||||
// enchantments) records
|
||||
align(1) struct ENAMstruct
|
||||
{
|
||||
// Magical effect
|
||||
short effectID; // ID of magic effect
|
||||
|
||||
// Which skills/attributes are affected (for restore/drain spells etc.)
|
||||
byte skill, attribute; // -1 if N/A
|
||||
|
||||
// Other spell parameters
|
||||
int range; // 0 - self, 1 - touch, 2 - target
|
||||
int area, duration, magnMin, magnMax;
|
||||
|
||||
static assert(ENAMstruct.sizeof==24);
|
||||
}
|
||||
|
||||
// Common stuff for all the load* structs
|
||||
template LoadTT(T)
|
||||
{
|
||||
LoadState state;
|
||||
char[] name, id;
|
||||
|
||||
MonsterObject *proto;
|
||||
static MonsterClass mc;
|
||||
|
||||
void makeProto(char[] clsName = null)
|
||||
{
|
||||
// Set up a prototype object
|
||||
if(mc is null)
|
||||
{
|
||||
// Use the template type name as the Monster class name if
|
||||
// none is specified.
|
||||
if(clsName == "")
|
||||
{
|
||||
clsName = typeid(T).toString;
|
||||
|
||||
// Remove the module name
|
||||
int i = clsName.rfind('.');
|
||||
if(i != -1)
|
||||
clsName = clsName[i+1..$];
|
||||
}
|
||||
|
||||
// All the game objects are in the 'game' package
|
||||
clsName = "game." ~ clsName;
|
||||
mc = vm.load(clsName);
|
||||
}
|
||||
|
||||
proto = mc.createObject();
|
||||
|
||||
proto.setString8("id", id);
|
||||
proto.setString8("name", name);
|
||||
|
||||
static if(is(typeof(data.weight) == float))
|
||||
{
|
||||
proto.setFloat("weight", data.weight);
|
||||
proto.setInt("value", data.value);
|
||||
}
|
||||
|
||||
static if(is(typeof(data.enchant)==int))
|
||||
proto.setInt("enchant", data.enchant);
|
||||
}
|
||||
}
|
||||
|
||||
template LoadT() { mixin LoadTT!(typeof(*this)); }
|
@ -1,167 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (esmmain.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 esm.esmmain;
|
||||
|
||||
public import esm.records;
|
||||
|
||||
import ogre.ogre;
|
||||
|
||||
/* This file is the main module for loading from ESM, ESP and ESS
|
||||
files. It stores all the data in the appropriate data structures
|
||||
for later referal. TODO: Put this in a class or whatever? Nah, we
|
||||
definately only need one structure like this at any one
|
||||
time. However, we have to deal with unloading and reloading it,
|
||||
even though that should be the exceptional case (change of plugins,
|
||||
etc), not the rule (loading a savegame should not alter the base
|
||||
data set, I think, but it's to early do decide.)*/
|
||||
|
||||
// Load a set of esm and esp files. For now, we just traverse in the
|
||||
// order given. Later, we should sort these into 'masters' and
|
||||
// 'plugins', because esms are always supposed to be loaded
|
||||
// first. TODO: I'm not sure if I should load all these in one
|
||||
// function. Do we need to be able to respond to errors in each file?
|
||||
// Nah, if anything fails, give a general error message, remove the
|
||||
// file from the list and try again. We have to be able to get a list
|
||||
// of which files depend upon which, though... this can be done before
|
||||
// this function is called.
|
||||
void loadTESFiles(char[][] files)
|
||||
{
|
||||
// Set up all the lists to hold our data
|
||||
initializeLists();
|
||||
|
||||
foreach(char[] filename; files)
|
||||
{
|
||||
esFile.open(filename, esmRegion);
|
||||
while(esFile.hasMoreRecs())
|
||||
{
|
||||
uint flags;
|
||||
|
||||
// Read record header
|
||||
char[] recName = esFile.getRecName();
|
||||
esFile.getRecHeader(flags);
|
||||
|
||||
if(flags & RecordFlags.Unknown)
|
||||
esFile.fail(format("UNKNOWN record flags: %xh", flags));
|
||||
|
||||
loadRecord(recName);
|
||||
}
|
||||
|
||||
// We have to loop through the lists and check for broken
|
||||
// references at this point, and if all forward references were
|
||||
// loaded. There might be other end-of-file things to do also.
|
||||
endFiles();
|
||||
}
|
||||
|
||||
esFile.close();
|
||||
|
||||
// Put all inventory items into one list
|
||||
items.addList(appas, ItemType.Apparatus);
|
||||
items.addList(lockpicks, ItemType.Pick);
|
||||
items.addList(probes, ItemType.Probe);
|
||||
items.addList(repairs, ItemType.Repair);
|
||||
items.addList(lights, ItemType.Light);
|
||||
items.addList(ingreds, ItemType.Ingredient);
|
||||
items.addList(potions, ItemType.Potion);
|
||||
items.addList(armors, ItemType.Armor);
|
||||
items.addList(weapons, ItemType.Weapon);
|
||||
items.addList(books, ItemType.Book);
|
||||
items.addList(clothes, ItemType.Clothing);
|
||||
items.addList(miscItems, ItemType.Misc);
|
||||
items.addList(itemLists, ItemType.ItemList); // Leveled item lists
|
||||
|
||||
// Same with all actors
|
||||
actors.addList(creatures, ItemType.Creature);
|
||||
actors.addList(creatureLists, ItemType.CreatureList);
|
||||
actors.addList(npcs, ItemType.NPC);
|
||||
|
||||
// Finally, add everything that might be looked up in a cell into
|
||||
// one list
|
||||
cellRefs.addList(items);
|
||||
cellRefs.addList(actors);
|
||||
cellRefs.addList(doors, ItemType.Door);
|
||||
cellRefs.addList(activators, ItemType.Activator);
|
||||
cellRefs.addList(statics, ItemType.Static);
|
||||
cellRefs.addList(containers, ItemType.Container);
|
||||
|
||||
// Check that all references are resolved
|
||||
items.endMerge();
|
||||
actors.endMerge();
|
||||
cellRefs.endMerge();
|
||||
|
||||
// Put all NPC dialogues into the hyperlink list
|
||||
foreach(char[] id, ref Dialogue dl; dialogues.names)
|
||||
hyperlinks.add(id, &dl);
|
||||
|
||||
// Finally, sort the hyperlink lists
|
||||
hyperlinks.sort();
|
||||
}
|
||||
|
||||
// Contains the small bits of information that we currently extract
|
||||
// from savegames.
|
||||
struct PlayerSaveInfo
|
||||
{
|
||||
char[] cellName;
|
||||
char[] playerName;
|
||||
Placement pos;
|
||||
}
|
||||
|
||||
// Load a TES savegame file (.ess). Currently VERY limited, reads the
|
||||
// player's cell name and position
|
||||
PlayerSaveInfo importSavegame(char[] file)
|
||||
{
|
||||
PlayerSaveInfo pi;
|
||||
|
||||
esFile.open(file, esmRegion);
|
||||
scope(exit) esFile.close();
|
||||
|
||||
if(esFile.getFileType != FileType.Ess)
|
||||
throw new TES3FileException(file ~ " is not a savegame");
|
||||
|
||||
with(esFile.saveData)
|
||||
{
|
||||
pi.cellName = stripz(cell);
|
||||
pi.playerName = stripz(player);
|
||||
}
|
||||
|
||||
with(esFile)
|
||||
{
|
||||
while(hasMoreRecs())
|
||||
{
|
||||
if(isNextHRec("REFR"))
|
||||
{
|
||||
while(hasMoreSubs())
|
||||
{
|
||||
getSubName();
|
||||
if(retSubName() == "DATA")
|
||||
readHExact(&pi.pos, pi.pos.sizeof);
|
||||
else
|
||||
skipHSub();
|
||||
}
|
||||
}
|
||||
else
|
||||
skipHRecord();
|
||||
}
|
||||
}
|
||||
return pi;
|
||||
}
|
@ -1,783 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (filereader.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 esm.filereader;
|
||||
|
||||
private:
|
||||
import std.stdio;
|
||||
import std.stream;
|
||||
import std.string;
|
||||
|
||||
import util.regions;
|
||||
import util.utfconvert;
|
||||
import monster.util.string;
|
||||
import core.resource;
|
||||
|
||||
import esm.listkeeper;
|
||||
import esm.defs;
|
||||
|
||||
public:
|
||||
|
||||
/*
|
||||
* Exception class for TES3File
|
||||
*/
|
||||
|
||||
class TES3FileException: Exception
|
||||
{
|
||||
this(char[] msg) {super("Error reading TES3 file: " ~ msg);}
|
||||
this() {this("Unknown error");}
|
||||
}
|
||||
|
||||
// Some flags are in use that we don't know. But we don't really know
|
||||
// any of them.
|
||||
enum RecordFlags : uint
|
||||
{
|
||||
Flag6 = 0x20, // Eg. adventurers_v2.0.esp (only once per file?)
|
||||
Persistent = 0x400,
|
||||
Flag13 = 0x1000, // Eg. Astarsis_BR.esm (several times per file?)
|
||||
Blocked = 0x2000,
|
||||
|
||||
Unknown = 0xffffffff - 0x3420
|
||||
}
|
||||
|
||||
enum FileType
|
||||
{
|
||||
Unknown,
|
||||
Esp, Plugin = Esp,
|
||||
Esm, Master = Esm,
|
||||
Ess, Savegame = Ess
|
||||
}
|
||||
|
||||
// Special files
|
||||
enum SpecialFile
|
||||
{
|
||||
Other,
|
||||
Morrowind,
|
||||
Tribunal,
|
||||
Bloodmoon
|
||||
}
|
||||
|
||||
enum Version { Unknown, v12, v13 }
|
||||
|
||||
// This struct should contain enough data to put a TES3File object
|
||||
// back into a specific file position and state. We use it to save the
|
||||
// "position" of objects in a file (eg. a cell), so we can return
|
||||
// there later and continue where we stopped (eg. when we want to load
|
||||
// that specific cell.)
|
||||
struct TES3FileContext
|
||||
{
|
||||
char[] filename;
|
||||
uint leftRec, leftSub;
|
||||
ulong leftFile;
|
||||
NAME recName, subName;
|
||||
FileType type;
|
||||
Version ver;
|
||||
|
||||
ulong filepos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance used to read TES3 files. Since we will only be reading one
|
||||
* file at a time, we might as well make one global instance.
|
||||
*/
|
||||
TES3File esFile;
|
||||
|
||||
/**
|
||||
* This struct reads an Elder Scrolls 3 file (esp, esm or ess)
|
||||
*
|
||||
* Makes heavy use of private variables to represent current
|
||||
* state.
|
||||
*
|
||||
* Relevant exceptions are
|
||||
* TES3FileException - error interpreting file
|
||||
* StreamFileException - file IO error
|
||||
*/
|
||||
struct TES3File
|
||||
{
|
||||
private:
|
||||
BufferedFile file;// Input file
|
||||
|
||||
// These are only used by getRecHeader and getSubHeader for
|
||||
// asserting the file's integrity.
|
||||
ulong leftFile; // Number of unread bytes in file
|
||||
uint leftRec; // Number of unread bytes in record
|
||||
|
||||
// This is used by sub-record readers for integrity checking.
|
||||
uint leftSub; // Number of bytes in subrecord
|
||||
|
||||
// Name of current record and current sub-record.
|
||||
NAME recName, subName;
|
||||
|
||||
char[] filename; // Filename
|
||||
FileType type; // File type
|
||||
Version ver; // File format version
|
||||
char[] author; // File author (max 32 bytes (with null?))
|
||||
char[] desc; // Description (max 256 bytes (ditto?))
|
||||
uint records; // Number of records in the file (doesn't seem to be right?)
|
||||
SpecialFile spf; // Is this a file we have to treat in a special way?
|
||||
|
||||
struct _mast
|
||||
{
|
||||
char[] name; // File name of an esm master for this file
|
||||
ulong size; // The master file's size in bytes (used for
|
||||
// version control)
|
||||
}
|
||||
|
||||
// List of esm masters for this file. For savegames this list also
|
||||
// contains all plugins.
|
||||
_mast masters[];
|
||||
|
||||
|
||||
// TES3.HEDR, file header struct
|
||||
align(1) struct HEDRstruct
|
||||
{
|
||||
union
|
||||
{
|
||||
float ver; // File format version, 1.2 and 1.3 supported.
|
||||
uint verHex; // 1.2 = 0x3f99999a, 1.3 = 0x3fa66666
|
||||
}
|
||||
int type; // 0=esp, 1=esm, 32=ess
|
||||
NAME32 author; // Author's name
|
||||
NAME256 desc; // File description blurb
|
||||
uint records; // Number of records in file (?)
|
||||
}
|
||||
|
||||
static assert(HEDRstruct.sizeof == 300);
|
||||
|
||||
// Which memory region to use for allocations.
|
||||
RegionManager region;
|
||||
|
||||
public:
|
||||
|
||||
// A struct found in the headers of savegame files. Contains quick
|
||||
// information to get us going, like the cell name and the player
|
||||
// name.
|
||||
struct _saveData
|
||||
{
|
||||
float[6] unknown;
|
||||
char[64] cell; // Cell name
|
||||
float unk2; // Unknown value
|
||||
char[32] player; // Player name
|
||||
}
|
||||
static assert(_saveData.sizeof == 124);
|
||||
_saveData saveData;
|
||||
|
||||
// Get file information
|
||||
char[] getFilename() { return filename; }
|
||||
ulong getFileSize() { return file.size; }
|
||||
ulong getPosition() { return file.position; }
|
||||
SpecialFile getSpecial() { return spf; }
|
||||
|
||||
char[] retSubName() { return subName; }
|
||||
|
||||
bool isVer12() { return ver == Version.v12;}
|
||||
bool isVer13() { return ver == Version.v13;}
|
||||
FileType getFileType() { return type; }
|
||||
_mast[] getMasters() { return masters; }
|
||||
uint getRecords() { return records; }
|
||||
char[] getAuthor() { return author; }
|
||||
RegionManager getRegion() { return region; }
|
||||
|
||||
// Store the current file state (position, file name, version, debug
|
||||
// info). The info should be enough to get us back on track for
|
||||
// reading from a file, without having to reread the header or any
|
||||
// previous records.
|
||||
void getContext(ref TES3FileContext c)
|
||||
{
|
||||
c.filename = filename;
|
||||
c.leftFile = leftFile;
|
||||
c.leftRec = leftRec;
|
||||
c.leftSub = leftSub;
|
||||
c.recName[] = recName;
|
||||
c.subName[] = subName;
|
||||
c.type = type;
|
||||
c.ver = ver;
|
||||
c.filepos = file.position;
|
||||
}
|
||||
|
||||
// Opens the file if it is not already opened. A region manager has
|
||||
// to be specified.
|
||||
void restoreContext(TES3FileContext c, RegionManager r)
|
||||
{
|
||||
if(filename != c.filename)
|
||||
openFile(c.filename, r);
|
||||
file.seekSet(cast(long)c.filepos);
|
||||
|
||||
// File is now open, copy state information
|
||||
filename = c.filename;
|
||||
leftFile = c.leftFile;
|
||||
leftRec = c.leftRec;
|
||||
leftSub = c.leftSub;
|
||||
recName[] = c.recName;
|
||||
subName[] = c.subName;
|
||||
type = c.type;
|
||||
ver = c.ver;
|
||||
}
|
||||
|
||||
// Open a new file and assign a region
|
||||
private void openFile(char[] filename, RegionManager r)
|
||||
{
|
||||
close();
|
||||
debug writefln("Opening file");
|
||||
if(file is null) file = new BufferedFile(new File());
|
||||
file.open(filename);
|
||||
|
||||
region = r;
|
||||
}
|
||||
|
||||
void open(char[] filename, RegionManager r)
|
||||
{
|
||||
uint flags;
|
||||
|
||||
debug writefln("openFile(%s, %s)", filename, r);
|
||||
openFile(filename, r);
|
||||
|
||||
if(iEnds(filename, "Morrowind.esm")) spf = SpecialFile.Morrowind;
|
||||
else if(iEnds(filename, "Tribunal.esm")) spf = SpecialFile.Tribunal;
|
||||
else if(iEnds(filename, "Bloodmoon.esm")) spf = SpecialFile.Bloodmoon;
|
||||
else spf = SpecialFile.Other;
|
||||
|
||||
debug writefln("Reading header");
|
||||
|
||||
// Do NOT .dup this filename, since it is referenced outside the
|
||||
// GC's reach and might be deleted.
|
||||
this.filename = filename;
|
||||
|
||||
leftFile = file.size;
|
||||
|
||||
// First things first
|
||||
if(getRecName() != "TES3")
|
||||
fail("Not a valid Morrowind file");
|
||||
|
||||
// Record header
|
||||
getRecHeader(flags);
|
||||
if(flags)
|
||||
writefln("WARNING: Header flags are non-zero");
|
||||
|
||||
// Read and analyse the header data
|
||||
HEDRstruct hedr;
|
||||
readHNExact(&hedr, hedr.sizeof, "HEDR");
|
||||
|
||||
// The float hedr.ver signifies the file format version. It can
|
||||
// take on these two values:
|
||||
// 0x3f99999a = 1.2
|
||||
// 0x3fa66666 = 1.3
|
||||
if( hedr.verHex == 0x3f99999a )
|
||||
ver = Version.v12;
|
||||
else if( hedr.verHex == 0x3fa66666 )
|
||||
ver = Version.v13;
|
||||
else
|
||||
{
|
||||
ver = Version.Unknown;
|
||||
writefln("WARNING: Unknown version: ", hedr.ver);
|
||||
writefln(" Hex: %X h", *(cast(uint*)&hedr.ver));
|
||||
}
|
||||
|
||||
switch(hedr.type)
|
||||
{
|
||||
case 0: type = FileType.Esp; break;
|
||||
case 1: type = FileType.Esm; break;
|
||||
case 32: type = FileType.Ess; break;
|
||||
default:
|
||||
type = FileType.Unknown;
|
||||
writefln("WARNING: Unknown file type: ", hedr.type);
|
||||
}
|
||||
|
||||
author = region.copy(stripz(hedr.author));
|
||||
desc = region.copy(stripz(hedr.desc));
|
||||
records = hedr.records;
|
||||
|
||||
masters = null;
|
||||
// Reads a MAST and a DATA fields
|
||||
while(isNextSub("MAST"))
|
||||
{
|
||||
_mast ma;
|
||||
|
||||
// MAST entry - master file name
|
||||
ma.name = getHString();
|
||||
|
||||
// DATA entry - master file size
|
||||
ma.size = getHNUlong("DATA");
|
||||
|
||||
// Add to the master list!
|
||||
masters ~= ma;
|
||||
}
|
||||
|
||||
if(type == FileType.Savegame)
|
||||
{
|
||||
// Savegame-related data
|
||||
|
||||
// Cell name, player name and player position
|
||||
readHNExact(&saveData, 124, "GMDT");
|
||||
|
||||
// Contains eg. 0xff0000, 0xff00, 0xff, 0x0, 0x20. No idea.
|
||||
getSubNameIs("SCRD");
|
||||
skipHSubSize(20);
|
||||
|
||||
// Screenshot. Fits with 128x128x4 bytes
|
||||
getSubNameIs("SCRS");
|
||||
skipHSubSize(65536);
|
||||
}
|
||||
}
|
||||
|
||||
// Close the file. We do not clear any object data at this point.
|
||||
void close()
|
||||
{
|
||||
debug writefln("close()");
|
||||
if(file !is null)
|
||||
file.close();
|
||||
leftFile = leftRec = leftSub = 0;
|
||||
debug writefln("Clearing strings");
|
||||
|
||||
recName[] = '\0';
|
||||
subName[] = '\0';
|
||||
|
||||
// This tells restoreContext() that we have to reopen the file
|
||||
filename = null;
|
||||
|
||||
debug writefln("exit close()");
|
||||
}
|
||||
|
||||
/*
|
||||
* Error reporting
|
||||
*/
|
||||
|
||||
void fail(char[] msg)
|
||||
{
|
||||
throw new TES3FileException
|
||||
(msg ~ "\nFile: " ~ filename ~ "\nRecord name: " ~ recName
|
||||
~ "\nSubrecord name: " ~ subName);
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
*
|
||||
* Highest level readers, reads a name and looks it up in the given
|
||||
* list.
|
||||
*
|
||||
************************************************************************/
|
||||
|
||||
// This should be more than big enough for references.
|
||||
private char lookupBuffer[200];
|
||||
|
||||
// Get a temporary string. This is faster and more memory efficient
|
||||
// that the other string functions (because it is allocation free),
|
||||
// but the returned string is only valid until tmpHString() is
|
||||
// called again.
|
||||
char[] tmpHString()
|
||||
{
|
||||
getSubHeader();
|
||||
assert(leftSub <= lookupBuffer.length, "lookupBuffer wasn't large enough");
|
||||
|
||||
// Use this to test the difference in memory consumption.
|
||||
return getString(lookupBuffer[0..leftSub]);
|
||||
}
|
||||
|
||||
// These are used for file lookups
|
||||
MeshIndex getMesh()
|
||||
{ getSubNameIs("MODL"); return resources.lookupMesh(tmpHString()); }
|
||||
SoundIndex getSound()
|
||||
{ getSubNameIs("FNAM"); return resources.lookupSound(tmpHString()); }
|
||||
IconIndex getIcon(char[] s = "ITEX")
|
||||
{ getSubNameIs(s); return resources.lookupIcon(tmpHString()); }
|
||||
TextureIndex getTexture()
|
||||
{ getSubNameIs("DATA"); return resources.lookupTexture(tmpHString()); }
|
||||
|
||||
// The getO* functions read optional records. If they are not
|
||||
// present, return null.
|
||||
|
||||
MeshIndex getOMesh()
|
||||
{ return isNextSub("MODL") ? resources.lookupMesh(tmpHString()) : MeshIndex.init; }
|
||||
/*
|
||||
SoundIndex getOSound()
|
||||
{ return isNextSub("FNAM") ? resources.lookupSound(tmpHString()) : SoundIndex.init; }
|
||||
*/
|
||||
IconIndex getOIcon()
|
||||
{ return isNextSub("ITEX") ? resources.lookupIcon(tmpHString()) : IconIndex.init; }
|
||||
TextureIndex getOTexture(char[] s="TNAM")
|
||||
{ return isNextSub(s) ? resources.lookupTexture(tmpHString()) : TextureIndex.init; }
|
||||
|
||||
// Reference with name s
|
||||
template getHNPtr(Type)
|
||||
{
|
||||
Type* getHNPtr(char[] s, ListKeeper list)
|
||||
{ getSubNameIs(s); return cast(Type*) list.lookup(tmpHString()); }
|
||||
}
|
||||
|
||||
// Reference, only get header
|
||||
template getHPtr(Type)
|
||||
{
|
||||
Type* getHPtr(ListKeeper list)
|
||||
{ return cast(Type*) list.lookup(tmpHString()); }
|
||||
}
|
||||
|
||||
// Optional reference with name s
|
||||
template getHNOPtr(Type)
|
||||
{
|
||||
Type* getHNOPtr(char[] s, ListKeeper list)
|
||||
{ return isNextSub(s) ? cast(Type*)list.lookup(tmpHString()) : null; }
|
||||
}
|
||||
|
||||
/************************************************************************
|
||||
*
|
||||
* Somewhat high level reading methods. Knows about headers and
|
||||
* leftFile/leftRec/leftSub.
|
||||
*
|
||||
************************************************************************/
|
||||
|
||||
// "Automatic" versions. Sets and returns recName and subName and
|
||||
// updates leftFile/leftRec.
|
||||
char[] getRecName()
|
||||
{
|
||||
if(!hasMoreRecs())
|
||||
fail("No more records, getRecName() failed");
|
||||
getName(recName);
|
||||
leftFile-= 4;
|
||||
return recName;
|
||||
}
|
||||
|
||||
// This is specially optimized for LoadINFO
|
||||
bool isEmptyOrGetName()
|
||||
{
|
||||
if(leftRec)
|
||||
{
|
||||
file.readBlock(subName.ptr, 4);
|
||||
leftRec -= 4;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// I've tried to optimize this slightly, since it gets called a LOT.
|
||||
void getSubName()
|
||||
{
|
||||
if(leftRec <= 0)
|
||||
fail("No more sub-records, getSubName() failed");
|
||||
|
||||
// Don't bother with error checking, we will catch an EOF upon
|
||||
// reading the subrecord data anyway.
|
||||
file.readBlock(subName.ptr, 4);
|
||||
|
||||
leftRec -= 4;
|
||||
}
|
||||
|
||||
// We often expect a certain subrecord type, this makes it easy to
|
||||
// check.
|
||||
void getSubNameIs(char[] s)
|
||||
{
|
||||
getSubName();
|
||||
if( subName != s )
|
||||
fail("Expected subrecord "~s~" but got "~subName);
|
||||
}
|
||||
|
||||
// Checks if the next sub-record is called s. If it is, run
|
||||
// getSubName, if not, return false.
|
||||
bool isNextSub(char[] s)
|
||||
{
|
||||
if(!leftRec) return false;
|
||||
|
||||
getName(subName);
|
||||
if(subName != s)
|
||||
{
|
||||
file.seekCur(-4);
|
||||
return false;
|
||||
}
|
||||
leftRec -= 4;
|
||||
|
||||
//getSubName();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Same as isNextSub, only it works on records instead of
|
||||
// sub-records. It also loads the record header.
|
||||
bool isNextHRec(char[] s)
|
||||
{
|
||||
if(!leftFile) return false;
|
||||
getName(recName);
|
||||
if(recName != s)
|
||||
{
|
||||
file.seekCur(-4);
|
||||
return false;
|
||||
}
|
||||
leftFile -= 4;
|
||||
|
||||
uint flags;
|
||||
getRecHeader(flags);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hasMoreSubs() { return leftRec > 0; }
|
||||
bool hasMoreRecs() { return leftFile > 0; }
|
||||
|
||||
// Remaining size of current record
|
||||
uint getRecLeft() { return leftRec; }
|
||||
// Size of current sub record
|
||||
uint getSubSize() { return leftSub; }
|
||||
|
||||
// Skip the rest of this record. Assumes the name and header have
|
||||
// already been read
|
||||
void skipRecord()
|
||||
{
|
||||
file.seekCur(leftRec);
|
||||
leftRec = 0;
|
||||
}
|
||||
|
||||
// Skip an entire record
|
||||
void skipHRecord()
|
||||
{
|
||||
if(!leftFile) return;
|
||||
|
||||
uint flags;
|
||||
|
||||
getRecName();
|
||||
getRecHeader(flags);
|
||||
skipRecord();
|
||||
}
|
||||
|
||||
// Skip current sub record and return size
|
||||
uint skipHSub()
|
||||
{
|
||||
getSubHeader();
|
||||
file.seekCur(leftSub);
|
||||
return leftSub;
|
||||
}
|
||||
|
||||
// Skip sub record and check it's size
|
||||
void skipHSubSize(uint size)
|
||||
{
|
||||
getSubHeader();
|
||||
if(leftSub != size)
|
||||
fail(format("Size mismatch: got %d, wanted %d", leftSub, size));
|
||||
file.seekCur(leftSub);
|
||||
}
|
||||
|
||||
// Check the name and size before skipping
|
||||
void skipHNSub(char[] name, uint size)
|
||||
{
|
||||
getSubNameIs(name);
|
||||
skipHSubSize(size);
|
||||
}
|
||||
|
||||
// These read an entire sub-record, including the header. They also
|
||||
// adjust and check leftSub and leftRecord variables through calling
|
||||
// getSubHeader().
|
||||
void readHExact(void * p, uint size)
|
||||
{
|
||||
getSubHeader();
|
||||
if(leftSub != size)
|
||||
fail(format("Size mismatch: got %d, wanted %d", leftSub, size));
|
||||
readExact(p, leftSub);
|
||||
}
|
||||
|
||||
template TgetHType(T)
|
||||
{ T TgetHType() { T t; readHExact(&t, t.sizeof); return t;} }
|
||||
|
||||
// To make these easier to use (and to further distinguish them from
|
||||
// the above "raw" versions), these return their value instead of
|
||||
// using an ref argument.
|
||||
alias TgetHType!(uint) getHUint;
|
||||
alias TgetHType!(int) getHInt;
|
||||
alias TgetHType!(float) getHFloat;
|
||||
alias TgetHType!(ulong) getHUlong;
|
||||
alias TgetHType!(byte) getHByte;
|
||||
|
||||
// Reads a string sub-record, including header
|
||||
char[] getHString()
|
||||
{
|
||||
getSubHeader();
|
||||
|
||||
// Hack to make MultiMark.esp load. Zero-length strings do not
|
||||
// occur in any of the official mods, but MultiMark makes use of
|
||||
// them. For some reason, they break the rules, and contain a
|
||||
// byte (value 0) even if the header says there is no data. If
|
||||
// Morrowind accepts it, so should we.
|
||||
if(leftSub == 0)
|
||||
{
|
||||
// Skip the following zero byte
|
||||
leftRec--;
|
||||
assert(file.getc() == 0);
|
||||
// TODO: Report this by setting a flag or something?
|
||||
return null;
|
||||
}
|
||||
|
||||
return getString(region.getString(leftSub));
|
||||
}
|
||||
|
||||
// Other quick aliases (this is starting to get messy)
|
||||
// Get string sub record string with name s
|
||||
char[] getHNString(char[] s)
|
||||
{ getSubNameIs(s); return getHString(); }
|
||||
|
||||
// Get optional sub record string with name s
|
||||
char[] getHNOString(char[] s)
|
||||
{ return isNextSub(s) ? getHString() : null; }
|
||||
|
||||
template TgetHNType(T)
|
||||
{ T TgetHNType(char[] s) { T t; readHNExact(&t, t.sizeof, s); return t;} }
|
||||
|
||||
template TgetHNOType(T)
|
||||
{
|
||||
T TgetHNOType(char[] s, T def)
|
||||
{
|
||||
if(isNextSub(s))
|
||||
{
|
||||
T t;
|
||||
readHExact(&t, t.sizeof);
|
||||
return t;
|
||||
}
|
||||
else return def;
|
||||
}
|
||||
}
|
||||
|
||||
alias TgetHNType!(uint) getHNUint;
|
||||
alias TgetHNType!(int) getHNInt;
|
||||
alias TgetHNType!(float) getHNFloat;
|
||||
alias TgetHNType!(ulong) getHNUlong;
|
||||
alias TgetHNType!(byte) getHNByte;
|
||||
alias TgetHNType!(short) getHNShort;
|
||||
alias TgetHNType!(byte) getHNByte;
|
||||
|
||||
alias TgetHNOType!(float) getHNOFloat;
|
||||
alias TgetHNOType!(int) getHNOInt;
|
||||
alias TgetHNOType!(byte) getHNOByte;
|
||||
|
||||
void readHNExact(void* p, uint size, char[] s)
|
||||
{ getSubNameIs(s); readHExact(p,size); }
|
||||
|
||||
// Record header
|
||||
// This updates the leftFile variable BEYOND the data that follows
|
||||
// the header, ie beyond the entire record. You are supposed to use
|
||||
// the leftRec variable when reading record data.
|
||||
void getRecHeader(out uint flags)
|
||||
{
|
||||
// General error checking
|
||||
if(leftFile < 12)
|
||||
fail("End of file while reading record header");
|
||||
if(leftRec)
|
||||
fail(format("Previous record contains %d unread bytes", leftRec));
|
||||
|
||||
getUint(leftRec);
|
||||
getUint(flags);// This header entry is always zero
|
||||
assert(flags == 0);
|
||||
getUint(flags);
|
||||
leftFile -= 12;
|
||||
|
||||
// Check that sizes add up
|
||||
if(leftFile < leftRec)
|
||||
fail(format(leftFile, " bytes left in file, but next record contains ",
|
||||
leftRec," bytes"));
|
||||
|
||||
// Adjust number of bytes left in file
|
||||
leftFile -= leftRec;
|
||||
}
|
||||
|
||||
// Sub-record head
|
||||
// This updates leftRec beyond the current sub-record as
|
||||
// well. leftSub contains size of current sub-record.
|
||||
void getSubHeader()
|
||||
{
|
||||
if(leftRec < 4)
|
||||
fail("End of record while reading sub-record header");
|
||||
|
||||
if(file.readBlock(&leftSub, 4) != 4)
|
||||
fail("getSubHeader could not read header length");
|
||||
|
||||
leftRec -= 4;
|
||||
|
||||
// Adjust number of record bytes left
|
||||
leftRec -= leftSub;
|
||||
|
||||
// Check that sizes add up
|
||||
if(leftRec < 0)
|
||||
fail(format(leftRec+leftSub,
|
||||
" bytes left in record, but next sub-record contains ",
|
||||
leftSub," bytes"));
|
||||
}
|
||||
|
||||
void getSubHeaderIs(uint size)
|
||||
{
|
||||
getSubHeader();
|
||||
if(leftSub != size)
|
||||
fail(format("Expected header size to be ", size, ", not ", leftSub));
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
* Low level reading methods
|
||||
*
|
||||
*************************************************************************/
|
||||
|
||||
/// Raw data of any size
|
||||
void readExact(void *buf, uint size)
|
||||
{
|
||||
assert(size != 0);
|
||||
file.readExact(buf,size);
|
||||
}
|
||||
|
||||
// One byte
|
||||
void getByte(out byte b) { file.read(b); }
|
||||
void getUByte(out ubyte b) { file.read(b); }
|
||||
// Two bytes
|
||||
void getUShort(out ushort s) { file.read(s); }
|
||||
// Four bytes
|
||||
void getUint(out uint u) { file.read(u); }
|
||||
void getInt(out int i) { file.read(i); }
|
||||
void getFloat(out float f) { file.read(f); }
|
||||
// Eight bytes
|
||||
void getUlong(out ulong l) { file.read(l); }
|
||||
|
||||
// Get a record or subrecord name, four bytes
|
||||
void getName(NAME name)
|
||||
{
|
||||
file.readBlock(name.ptr, 4);
|
||||
/*
|
||||
if(file.readBlock(name.ptr, 4) != 4)
|
||||
fail("getName() could not find more data");
|
||||
*/
|
||||
}
|
||||
|
||||
// Fill buffer of predefined length. If actual string is shorter
|
||||
// (ie. null terminated), the buffer length is set
|
||||
// accordingly. Chopped string is returned. All strings pass through
|
||||
// this function, so any character encoding conversions should
|
||||
// happen here.
|
||||
char[] getString(char[] str)
|
||||
{
|
||||
if(str.length != file.readBlock(str.ptr,str.length))
|
||||
fail("getString() could not find enough data in stream");
|
||||
|
||||
str = stripz(str);
|
||||
makeUTF8(str); // TODO: A hack. Will replace non-utf characters
|
||||
// with question marks. This is neither a very
|
||||
// desirable result nor a very optimized
|
||||
// implementation of it.
|
||||
return str;
|
||||
}
|
||||
|
||||
// Use this to allocate and read strings of predefined length
|
||||
char[] getString(int l)
|
||||
{
|
||||
char[] str = region.getString(l);
|
||||
return getString(str);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
module esm.imports;
|
||||
|
||||
/* This is a file that imports common modules used by the load*.d
|
||||
record loaders. It is really a cut down version of what used to be
|
||||
the start of records.d.
|
||||
|
||||
This file MUST NOT import records.d - directly or indirectly -
|
||||
because that will trigger a nice three page long list of template
|
||||
forwarding errors from the compiler.
|
||||
|
||||
What happens is that when DMD/GDC compiles one of the load* files,
|
||||
it is forced to read records.d first (since it is an imported
|
||||
module) - but then it sees a template that referes to a struct in
|
||||
the current load* file, before that struct is defined. Curriously
|
||||
enough, DMD has no problems when you specify all the source files
|
||||
on the command line simultaneously. This trick doesn't work with
|
||||
GDC though, and DSSS doesn't use it either.
|
||||
|
||||
This file was created to work around this compiler bug.
|
||||
*/
|
||||
|
||||
public
|
||||
{
|
||||
import esm.defs;
|
||||
import esm.filereader;
|
||||
import esm.listkeeper;
|
||||
|
||||
import core.resource;
|
||||
import core.memory;
|
||||
|
||||
import util.regions;
|
||||
import monster.util.aa;
|
||||
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
|
||||
alias RegionBuffer!(ENAMstruct) EffectList;
|
||||
|
||||
// Records that are cross referenced often
|
||||
import esm.loadscpt;
|
||||
import esm.loadsoun;
|
||||
import esm.loadspel;
|
||||
import esm.loadench;
|
||||
|
||||
import monster.monster;
|
||||
}
|
||||
|
@ -1,330 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (listkeeper.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 esm.listkeeper;
|
||||
|
||||
import monster.util.aa;
|
||||
|
||||
import core.memory;
|
||||
|
||||
import esm.filereader;
|
||||
import esm.defs;
|
||||
|
||||
import std.stdio;
|
||||
|
||||
// Item types, used in the lookup table for inventory items, creature
|
||||
// lists and leveled lists. We also use it for all types of references
|
||||
// that can exist in cells.
|
||||
enum ItemType
|
||||
{
|
||||
// Items
|
||||
None = 0, Potion, Apparatus, Armor, Weapon, Book, Clothing,
|
||||
Light, Ingredient, Pick, Probe, Repair, Misc, ItemList,
|
||||
|
||||
// Used for creature lists
|
||||
Creature, CreatureList, NPC,
|
||||
|
||||
// Other cell references
|
||||
Door, Activator, Static, Container//, SoundGen
|
||||
}
|
||||
|
||||
abstract class ListKeeper
|
||||
{
|
||||
int listIndex;
|
||||
|
||||
new(uint size)
|
||||
{
|
||||
return esmRegion.allocate(size).ptr;
|
||||
}
|
||||
|
||||
delete(void *p) { assert(0); }
|
||||
|
||||
this()
|
||||
{
|
||||
// Store our index for later use
|
||||
listIndex = recordLists.length;
|
||||
|
||||
// Add the class to the global list
|
||||
recordLists ~= this;
|
||||
}
|
||||
|
||||
// Load a record from a master or plugin file
|
||||
void load();
|
||||
|
||||
// Looks up a reference. If it does not exist it is assumed to be a
|
||||
// forward reference within a file, and is inserted.
|
||||
void* lookup(char[] s);
|
||||
|
||||
// Tell the loader that current file has ended, so it can do things
|
||||
// like check that all referenced objects have been loaded.
|
||||
void endFile();
|
||||
|
||||
// Number of inserted elements
|
||||
uint length();
|
||||
|
||||
void addToList(ref ItemBaseList l, ItemType t) { assert(0); }
|
||||
}
|
||||
|
||||
ListKeeper recordLists[];
|
||||
|
||||
// Keep the list of Type structures for records where the first
|
||||
// subrecord is an id string called NAME. This id is used for
|
||||
// lookup. Although almost all lookups match in case, there are a few
|
||||
// sounds that don't, so we treat these id lookups as generally case
|
||||
// insensitive. This hasn't posed any problems so far.
|
||||
class ListID(Type) : ListKeeper
|
||||
{
|
||||
HashTable!(char[], Type, ESMRegionAlloc, CITextHash) names;
|
||||
|
||||
this(uint size)
|
||||
{
|
||||
names = names.init;
|
||||
if(size) names.rehash(size);
|
||||
}
|
||||
|
||||
// Reads the id for this header. Override if the id is not simply
|
||||
// getHNString("NAME")
|
||||
char[] getID()
|
||||
{
|
||||
return esFile.getHNString("NAME");
|
||||
}
|
||||
|
||||
// Load a record from a master of plugin file
|
||||
void load()
|
||||
{
|
||||
assert(esFile.getFileType == FileType.Esm ||
|
||||
esFile.getFileType == FileType.Esp);
|
||||
|
||||
// Get the identifier of this record
|
||||
char[] id = getID();
|
||||
|
||||
// Get pointer to a new or existing object.
|
||||
Type *p;
|
||||
if(names.insertEdit(id, p))
|
||||
// A new item was inserted
|
||||
{
|
||||
p.state = LoadState.Unloaded;
|
||||
p.id = id;
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
}
|
||||
else
|
||||
// Item already existed, either from a previous file or as a
|
||||
// forward reference from this file. Load on top of it. The
|
||||
// LoadState tells the struct whether it contains loaded data.
|
||||
{
|
||||
/*
|
||||
if(p.state == LoadState.Loaded)
|
||||
// Make a special case for this, perhaps, or just ignore it.
|
||||
writefln("WARNING: Duplicate record in file %s: '%s'",
|
||||
esFile.getFilename(), id);
|
||||
*/
|
||||
|
||||
assert(icmp(p.id, id) == 0);
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// Looks up a reference. If it does not exist it is assumed to be a
|
||||
// forward reference within a file, and is inserted.
|
||||
void* lookup(char[] id)
|
||||
{
|
||||
if(!id.length) return null; // Empty reference
|
||||
|
||||
Type *p = names.lookup(id);
|
||||
// Is the value in the list?
|
||||
if(!p)
|
||||
// No, assume it is a forward reference.
|
||||
{
|
||||
// Since the lookup name is stored in an internal buffer in
|
||||
// esFile, we have to copy it.
|
||||
id = esmRegion.copy(id);
|
||||
|
||||
// To avoid copying the string on every lookup, we have to
|
||||
// insert in a separate step. But a double lookup isn't
|
||||
// really THAT expensive. Besides, my tests show that this
|
||||
// is used in less than 10% of the cases.
|
||||
names.insertEdit(id, p);
|
||||
p.id = id;
|
||||
p.state = LoadState.Unloaded;
|
||||
}
|
||||
return cast(void*)p;
|
||||
}
|
||||
|
||||
// Check that all referenced objects are actually loaded.
|
||||
void endFile()
|
||||
in
|
||||
{
|
||||
// We can skip this in release builds
|
||||
names.validate();
|
||||
}
|
||||
body
|
||||
{
|
||||
foreach(char[] id, ref Type t; names)
|
||||
// Current file is now counted as done
|
||||
if(t.state == LoadState.Loaded) t.state = LoadState.Previous;
|
||||
else if(t.state == LoadState.Unloaded)
|
||||
//writefln("WARNING: Unloaded reference " ~ id);
|
||||
esFile.fail("Unloaded reference " ~ id);
|
||||
}
|
||||
|
||||
// Number of inserted elements
|
||||
uint length() {return names.length;}
|
||||
|
||||
// Add the names in this list to an ItemList
|
||||
void addToList(ref ItemBaseList l, ItemType t)
|
||||
{
|
||||
foreach(char[] id, ref Type s; names)
|
||||
l.insert(id, &s, t);
|
||||
}
|
||||
}
|
||||
|
||||
// A pointer to an item
|
||||
struct ItemBase
|
||||
{
|
||||
ItemType type;
|
||||
void *p;
|
||||
}
|
||||
|
||||
struct ItemBaseList
|
||||
{
|
||||
HashTable!(char[],ItemBase,ESMRegionAlloc) list;
|
||||
|
||||
void addList(ItemBaseList l)
|
||||
{
|
||||
foreach(char[] id, ItemBase b; l.list)
|
||||
insert(id, b.p, b.type);
|
||||
}
|
||||
|
||||
void addList(ListKeeper source, ItemType type)
|
||||
{
|
||||
source.addToList(*this, type);
|
||||
}
|
||||
|
||||
void insert(char[] id, void* p, ItemType type)
|
||||
{
|
||||
ItemBase *b;
|
||||
if(!list.insertEdit(id, b))
|
||||
{
|
||||
//writefln("Replacing item ", id);
|
||||
if(b.type != ItemType.None)
|
||||
esFile.fail("Replaced valid item: " ~ id);
|
||||
}
|
||||
//else writefln("Inserting new item ", id);
|
||||
|
||||
b.type = type;
|
||||
b.p = p;
|
||||
}
|
||||
|
||||
// Called at the end to check that all referenced items have been resolved
|
||||
void endMerge()
|
||||
{
|
||||
foreach(char[] id, ref ItemBase t; list)
|
||||
// Current file is now counted as done
|
||||
if(t.type == ItemType.None)
|
||||
// TODO: Don't use esFile.fail for this
|
||||
esFile.fail("ItemBaseList: Unresolved forward reference: " ~ id);
|
||||
}
|
||||
|
||||
// Look up an item, return a pointer to the ItemBase representing
|
||||
// it. If it does not exist, it is inserted.
|
||||
ItemBase *lookup(char[] id)
|
||||
{
|
||||
if(!id.length) return null; // Empty reference
|
||||
ItemBase *b = list.lookup(id);
|
||||
// Is the value in the list?
|
||||
if(!b)
|
||||
// No, assume it is a forward reference.
|
||||
{
|
||||
// Since the lookup name is stored in an internal buffer in
|
||||
// esFile, we have to copy it.
|
||||
id = esmRegion.copy(id);
|
||||
|
||||
// To avoid copying the string on every lookup, we have to
|
||||
// insert in a separate step. But a double lookup isn't
|
||||
// really THAT expensive.
|
||||
list.insertEdit(id, b);
|
||||
|
||||
b.p = null;
|
||||
b.type = ItemType.None;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
// An item. Contains a reference to an ItemBase, which again is a
|
||||
// reference to an item. The ItemBase might change after we have
|
||||
// looked it up (for forward references), so we have to use a pointer.
|
||||
struct Item
|
||||
{
|
||||
ItemBase *i;
|
||||
|
||||
void* getPtr(ItemType type)
|
||||
{
|
||||
if(i != null && i.type == type) return i.p;
|
||||
return null;
|
||||
}
|
||||
|
||||
T* getType(T, ItemType Type)()
|
||||
{
|
||||
return cast(T*)getPtr(Type);
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemList
|
||||
{
|
||||
private:
|
||||
ItemBaseList list;
|
||||
|
||||
public:
|
||||
void addList(ItemList l)
|
||||
{ list.addList(l.list); }
|
||||
|
||||
void addList(ListKeeper source, ItemType type)
|
||||
{ list.addList(source, type); }
|
||||
|
||||
Item lookup(char[] id)
|
||||
{
|
||||
Item i;
|
||||
i.i = list.lookup(id);
|
||||
return i;
|
||||
}
|
||||
|
||||
void endMerge()
|
||||
{ list.endMerge(); }
|
||||
|
||||
void endFile()
|
||||
in { list.list.validate(); }
|
||||
body {}
|
||||
|
||||
void rehash(uint size)
|
||||
{ list.list.rehash(size); }
|
||||
|
||||
uint length() { return list.list.length(); }
|
||||
}
|
||||
|
||||
// Aggregate lists, made by concatinating several other lists.
|
||||
ItemList items; // All inventory items, including leveled item lists
|
||||
ItemList actors; // All actors, ie. NPCs, creatures and leveled lists
|
||||
ItemList cellRefs; // All things that are referenced from cells
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
This file contains some leftovers which have not yet been ported to
|
||||
C++.
|
||||
*/
|
||||
|
||||
align(1) struct AMBIStruct
|
||||
{
|
||||
Color ambient, sunlight, fog;
|
||||
float fogDensity;
|
||||
|
||||
static assert(AMBIStruct.sizeof == 16);
|
||||
}
|
||||
|
||||
int max(int x, int y)
|
||||
{ return x>=y?x:y; }
|
||||
|
||||
struct ExtCellHash
|
||||
{
|
||||
// This is a pretty good hash, gives no collisions for all of
|
||||
// Morrowind.esm when the table size is 2048, and it gives very few
|
||||
// collisions overall. Not that it matters that much.
|
||||
static uint hash(uint val)
|
||||
{
|
||||
uint res = cast(ushort)val;
|
||||
res += *(cast(ushort*)&val+1)*41;
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool isEqual(uint a, uint b) { return a==b; }
|
||||
}
|
||||
|
||||
class CellList : ListKeeper
|
||||
{
|
||||
// Again, these are here to avoid DMD template bugs
|
||||
alias _aaNode!(char[], InteriorCell) _unused1;
|
||||
alias _aaNode!(uint, ExteriorCell) _unused2;
|
||||
|
||||
HashTable!(char[], InteriorCell, ESMRegionAlloc) in_cells;
|
||||
HashTable!(uint, ExteriorCell, ESMRegionAlloc, ExtCellHash) ex_cells;
|
||||
|
||||
// Store the maximum x or y coordinate (in absolute value). This is
|
||||
// used in the landscape pregen process.
|
||||
int maxXY;
|
||||
|
||||
this()
|
||||
{
|
||||
in_cells = in_cells.init;
|
||||
in_cells.rehash(1600);
|
||||
|
||||
ex_cells = ex_cells.init;
|
||||
ex_cells.rehash(1800);
|
||||
}
|
||||
|
||||
align(1) struct DATAstruct
|
||||
{
|
||||
CellFlags flags;
|
||||
int gridX, gridY;
|
||||
static assert(DATAstruct.sizeof==12);
|
||||
}
|
||||
|
||||
DATAstruct data;
|
||||
|
||||
// Look up an interior cell, throws an error if not found (might
|
||||
// change later)
|
||||
InteriorCell *getInt(char[] s)
|
||||
{
|
||||
return in_cells.getPtr(s);
|
||||
}
|
||||
|
||||
// Exterior cell, same as above
|
||||
ExteriorCell *getExt(int x, int y)
|
||||
{
|
||||
return ex_cells.getPtr(compound(x,y));
|
||||
}
|
||||
|
||||
// Check whether we have a given exterior cell
|
||||
bool hasExt(int x, int y)
|
||||
{
|
||||
return ex_cells.inList(compound(x,y));
|
||||
}
|
||||
|
||||
void *lookup(char[] s)
|
||||
{ assert(0); }
|
||||
|
||||
void endFile()
|
||||
out
|
||||
{
|
||||
in_cells.validate();
|
||||
ex_cells.validate();
|
||||
}
|
||||
body
|
||||
{
|
||||
foreach(id, ref c; in_cells)
|
||||
{
|
||||
if(c.state == LoadState.Loaded) c.state = LoadState.Previous;
|
||||
// We never forward reference cells!
|
||||
assert(c.state != LoadState.Unloaded);
|
||||
}
|
||||
|
||||
foreach(id, ref c; ex_cells)
|
||||
{
|
||||
if(c.state == LoadState.Loaded) c.state = LoadState.Previous;
|
||||
// We never forward reference cells!
|
||||
assert(c.state != LoadState.Unloaded);
|
||||
}
|
||||
}
|
||||
|
||||
uint length() { return numInt() + numExt(); }
|
||||
uint numInt() { return in_cells.length; }
|
||||
uint numExt() { return ex_cells.length; }
|
||||
|
||||
// Turn an exterior cell grid position into a unique number
|
||||
static uint compound(int gridX, int gridY)
|
||||
{
|
||||
return cast(ushort)gridX + ((cast(ushort)gridY)<<16);
|
||||
}
|
||||
|
||||
static void decompound(uint val, out int gridX, out int gridY)
|
||||
{
|
||||
gridX = cast(short)(val&0xffff);
|
||||
gridY = cast(int)(val&0xffff0000) >> 16;
|
||||
}
|
||||
|
||||
void load()
|
||||
{with(esFile){
|
||||
char[] id = getHNString("NAME");
|
||||
|
||||
// Just ignore this, don't know what it does. I assume it
|
||||
// deletes the cell, but we can't handle that yet.
|
||||
if(isNextSub("DELE")) getHInt();
|
||||
|
||||
readHNExact(&data, data.sizeof, "DATA");
|
||||
|
||||
if(data.flags & CellFlags.Interior)
|
||||
{
|
||||
InteriorCell *p;
|
||||
if(in_cells.insertEdit(id, p))
|
||||
// New item was inserted
|
||||
{
|
||||
p.state = LoadState.Unloaded;
|
||||
p.id = id;
|
||||
p.flags = data.flags;
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
}
|
||||
else
|
||||
// Overloading an existing cell
|
||||
{
|
||||
if(p.state != LoadState.Previous)
|
||||
fail("Duplicate interior cell " ~ id);
|
||||
|
||||
assert(id == p.id);
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
}
|
||||
}
|
||||
else // Exterior cell
|
||||
{
|
||||
uint key = compound(data.gridX, data.gridY);
|
||||
|
||||
ExteriorCell *p;
|
||||
if(ex_cells.insertEdit(key, p))
|
||||
// New cell
|
||||
{
|
||||
p.state = LoadState.Unloaded;
|
||||
p.name = id;
|
||||
p.flags = data.flags;
|
||||
p.gridX = data.gridX;
|
||||
p.gridY = data.gridY;
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
|
||||
int mx = max(abs(p.gridX), abs(p.gridY));
|
||||
maxXY = max(maxXY, mx);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(p.state != LoadState.Previous)
|
||||
fail(format("Duplicate exterior cell %d %d",
|
||||
data.gridX, data.gridY));
|
||||
assert(p.gridX == data.gridX);
|
||||
assert(p.gridY == data.gridY);
|
||||
p.load();
|
||||
p.state = LoadState.Loaded;
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
CellList cells;
|
@ -1,289 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (loaddial.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 esm.loaddial;
|
||||
import esm.imports;
|
||||
import esm.loadinfo;
|
||||
|
||||
/*
|
||||
* Dialogue topic and journal entries. The acutal data is contained in
|
||||
* the following INFO records.
|
||||
*/
|
||||
|
||||
// Keep a list of possible hyper links. This list is used when parsing
|
||||
// text and finding references to topic names. Eg. if a character says
|
||||
// "Would you like to join the mages guild?", we must be able to pick
|
||||
// out the phrase "join the mages guild?" and make a hyperlink of
|
||||
// it. Each link is indexed by their first word. The structure
|
||||
// contains the rest of the phrase, so the phrase above would be
|
||||
// indexed by "join" and contain the string "the mages guild?", for
|
||||
// quick comparison with the text are currently parsing. It also
|
||||
// contains a pointer to the corresponding dialogue struct. The lists
|
||||
// are sorted by descending string length, in order to match the
|
||||
// longest possible term first.
|
||||
|
||||
struct Hyperlink
|
||||
{
|
||||
Dialogue *ptr;
|
||||
char[] rest;
|
||||
|
||||
// Returns a < b if a.length > b.length.
|
||||
int opCmp(Hyperlink *h) {return h.rest.length - rest.length;}
|
||||
}
|
||||
|
||||
alias RegionBuffer!(Hyperlink) HyperlinkArray;
|
||||
|
||||
// This is much nicer now that we use our own AA.
|
||||
struct HyperlinkList
|
||||
{
|
||||
// Make a case insensitive hash table of Hyperlink arrays.
|
||||
HashTable!(char[], HyperlinkArray, ESMRegionAlloc, CITextHash) list;
|
||||
|
||||
void add(char[] topic, Dialogue* ptr)
|
||||
{
|
||||
// Only add dialogues
|
||||
if(ptr.type != Dialogue.Type.Topic) return;
|
||||
|
||||
Hyperlink l;
|
||||
l.ptr = ptr;
|
||||
l.rest = topic;
|
||||
|
||||
// Use the first word as the index
|
||||
topic = nextWord(l.rest);
|
||||
|
||||
// Insert a new array, or get an already existing one
|
||||
HyperlinkArray ha = list.get(topic,
|
||||
// Create a new array
|
||||
delegate void (ref HyperlinkArray a)
|
||||
{ a = esmRegion.getBuffer!(Hyperlink)(0,1); }
|
||||
);
|
||||
|
||||
// Finally, add it to the list
|
||||
ha ~= l;
|
||||
}
|
||||
|
||||
Hyperlink[] getList(char[] word)
|
||||
{
|
||||
HyperlinkArray p;
|
||||
if(list.inList(word, p)) return p.array();
|
||||
return null;
|
||||
}
|
||||
|
||||
void rehash(uint size)
|
||||
{
|
||||
list.rehash(size);
|
||||
}
|
||||
|
||||
// We wouldn't need this if we only dealt with one file, since the
|
||||
// topics are already sorted in Morrowind.esm. However, other files
|
||||
// might add items out of order later, so we have to sort it. To
|
||||
// understand why this is needed, consider the following example:
|
||||
//
|
||||
// Morrowind.esm contains the topic 'join us'. When ever the text
|
||||
// ".. join us blahblah ..." is encountered, this match is
|
||||
// found. However, if a plugin adds the topic 'join us today', we
|
||||
// have to place this _before_ 'join us' in the list, or else it
|
||||
// will never be matched.
|
||||
void sort()
|
||||
{
|
||||
foreach(char[] s, HyperlinkArray l; list)
|
||||
{
|
||||
l.array().sort;
|
||||
/*
|
||||
writefln("%s: ", s, l.length);
|
||||
foreach(Hyperlink h; l.array())
|
||||
writefln(" %s (%s)", h.rest, h.ptr.id);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List of dialogue hyperlinks
|
||||
HyperlinkList hyperlinks;
|
||||
|
||||
struct Dialogue
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
Topic = 0,
|
||||
Voice = 1,
|
||||
Greeting = 2,
|
||||
Persuasion = 3,
|
||||
Journal = 4,
|
||||
Deleted = -1
|
||||
}
|
||||
|
||||
//Type type;
|
||||
DialogueType type;
|
||||
|
||||
DialInfoList infoList;
|
||||
|
||||
char[] id; // This is the 'dialogue topic' that the user actually
|
||||
// sees.
|
||||
LoadState state;
|
||||
|
||||
void load()
|
||||
{with(esFile){
|
||||
getSubNameIs("DATA");
|
||||
|
||||
getSubHeader();
|
||||
int si = getSubSize();
|
||||
if(si == 1)
|
||||
{
|
||||
byte b;
|
||||
getByte(b);
|
||||
DialogueType t = cast(DialogueType)b;
|
||||
|
||||
// Meet the new type, same as the old type
|
||||
if(t != this.type && state == LoadState.Previous)
|
||||
fail("Type changed in dialogue " ~ id);
|
||||
|
||||
this.type = t;
|
||||
}
|
||||
else if(si == 4)
|
||||
{
|
||||
// These are just markers, their values are not used.
|
||||
int i;
|
||||
getInt(i);
|
||||
//writefln("In file %s:", getFilename());
|
||||
//writefln(" WARNING: DIAL.DATA was size 4 and contains: ", i);
|
||||
i = getHNInt("DELE");
|
||||
//writefln(" DELE contains ", i);
|
||||
this.type = Type.Deleted;
|
||||
}
|
||||
else fail("Unknown sub record size " ~ toString(si));
|
||||
|
||||
infoList.state = state;
|
||||
while(isNextHRec("INFO"))
|
||||
infoList.load(this.type);
|
||||
//skipRecord();
|
||||
}}
|
||||
}
|
||||
|
||||
typedef Dialogue.Type DialogueType;
|
||||
|
||||
/+
|
||||
// I don't remember when I commented out this code or what state
|
||||
// it is in. Probably highly experimental.
|
||||
// --------------
|
||||
|
||||
// Loop through the info blocks in this dialogue, and update the
|
||||
// master as necessary.
|
||||
|
||||
// TODO: Note that the dialogue system in Morrowind isn't very
|
||||
// robust. If several mods insert dialogues at exactly the same
|
||||
// place, the mods loaded last might overwrite the previous mods,
|
||||
// completely removing the previous entry even if the two entries
|
||||
// do not have the same id. This is because a change also
|
||||
// overwrites the previous and the next entry, in order to update
|
||||
// their "previous" and "next" fields. Furthermore, we might put
|
||||
// ourselves in a situation where the forward and backward chains
|
||||
// do not match, or in a situation where we update a deleted
|
||||
// info. For now I do nothing about it, but I will have to develop
|
||||
// a "conflict manager" later on. It SHOULD be possible to merge
|
||||
// these info lists automatically in most cases, but it
|
||||
// complicates the code.
|
||||
|
||||
// Whoa, seems we have a case study already with just tribunal and
|
||||
// bloodmoon loaded! See comments below.
|
||||
|
||||
foreach(char[] id, ref DialInfoLoad m; mod.infoList)
|
||||
{
|
||||
// Remove the response if it is marked as deleted.
|
||||
if(m.deleted)
|
||||
{
|
||||
if((id in master.infoList) == null)
|
||||
writefln("Cannot delete info %s, does not exist", id);
|
||||
else master.infoList.remove(id);
|
||||
}
|
||||
else
|
||||
// Just plain copy it in.
|
||||
master.infoList[id] = m;
|
||||
}
|
||||
}
|
||||
|
||||
// Here we have to fix inconsistencies. A good case example is the
|
||||
// dialogue "Apelles Matius" in trib/blood. Trib creates a
|
||||
// dialogue of a few items, bloodmoon adds another. But since the
|
||||
// two are independent, the list in bloodmoon does not change the
|
||||
// one in trib but rather creates a new one. In other words, we
|
||||
// will have to deal with the possibility of several "independent"
|
||||
// lists within each topic. We can do this by looking for several
|
||||
// start points (ie. infos with prev="") and just latch them onto
|
||||
// each other. I'm not sure it gives the correct result,
|
||||
// though. For example, which list comes first would be rather
|
||||
// arbitrarily decided by the order we traverse the infoList AA. I
|
||||
// will just have to assume that they truly are "independent".
|
||||
|
||||
// There still seems to be a problem though. Bloodmoon overwrites
|
||||
// some stuff added by Tribunal, see "Boots of the Apostle" for an
|
||||
// example. Looks like the editor handles it just fine... We need
|
||||
// to make sure that all the items in our AA are put into the
|
||||
// list, and in the right place too. We obviously cannot fully
|
||||
// trust the 'next' and 'prev' fields, but they are the only
|
||||
// guidance we have. Deal with it later!
|
||||
|
||||
// At this point we assume "master" to contain the final dialogue
|
||||
// list, so at this point we can set it in stone.
|
||||
infoList.length = master.infoList.length;
|
||||
|
||||
// Find the first entry
|
||||
DialInfoLoad* starts[]; // starting points for linked lists
|
||||
DialInfoLoad *current;
|
||||
foreach(char[] id, ref DialInfoLoad l; master.infoList)
|
||||
if(l.prev == "") starts ~= &l;
|
||||
|
||||
foreach(int num, ref DialInfo m; infoList)
|
||||
{
|
||||
if(current == null)
|
||||
{
|
||||
if(starts.length == 0)
|
||||
{
|
||||
writefln("Error: No starting points!");
|
||||
infoList.length = num;
|
||||
break;
|
||||
}
|
||||
// Pick the next starting point
|
||||
current = starts[0];
|
||||
starts = starts[1..$];
|
||||
}
|
||||
m.copy(*current, this);
|
||||
|
||||
if((*current).next == "")
|
||||
current = null;
|
||||
else
|
||||
{
|
||||
current = (*current).next in master.infoList;
|
||||
if(current == null)
|
||||
{
|
||||
writefln("Error in dialouge info lookup!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(infoList.length != master.infoList.length)
|
||||
writefln("Dialogue list lengths do not match, %d != %d",
|
||||
infoList.length, master.infoList.length);
|
||||
}
|
||||
}
|
||||
+/
|
@ -1,219 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (records.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 esm.records;
|
||||
|
||||
public
|
||||
{
|
||||
import monster.util.aa;
|
||||
import util.regions;
|
||||
|
||||
import core.memory;
|
||||
import core.resource;
|
||||
|
||||
import esm.filereader;
|
||||
import esm.defs;
|
||||
import esm.listkeeper;
|
||||
|
||||
import std.stdio; // Remove later
|
||||
}
|
||||
|
||||
public import
|
||||
esm.loadacti, esm.loaddoor, esm.loadglob, esm.loadscpt, esm.loadsoun, esm.loadgmst,
|
||||
esm.loadfact, esm.loadstat, esm.loadspel, esm.loadalch, esm.loadappa, esm.loadarmo,
|
||||
esm.loadbody, esm.loadench, esm.loadbook, esm.loadbsgn, esm.loadltex, esm.loadmgef,
|
||||
esm.loadweap, esm.loadlocks,esm.loadcell, esm.loadregn, esm.loadligh, esm.loadskil,
|
||||
esm.loadsndg, esm.loadrace, esm.loadmisc, esm.loadclot, esm.loadingr, esm.loadclas,
|
||||
esm.loadcont, esm.loadcrea, esm.loadnpc, esm.loaddial, esm.loadinfo, esm.loadsscr,
|
||||
esm.loadlevlist;
|
||||
|
||||
void loadRecord(char[] recName)
|
||||
{
|
||||
switch(recName)
|
||||
{
|
||||
case "ACTI": activators.load(); break;
|
||||
case "DOOR": doors.load(); break;
|
||||
case "GLOB": globals.load(); break;
|
||||
case "SCPT": scripts.load(); break;
|
||||
case "SOUN": sounds.load(); break;
|
||||
case "GMST": gameSettings.load(); break;
|
||||
case "FACT": factions.load(); break;
|
||||
case "STAT": statics.load(); break;
|
||||
case "SPEL": spells.load(); break;
|
||||
case "ALCH": potions.load(); break;
|
||||
case "APPA": appas.load(); break;
|
||||
case "ARMO": armors.load(); break;
|
||||
case "BODY": bodyParts.load(); break;
|
||||
case "ENCH": enchants.load(); break;
|
||||
case "BOOK": books.load(); break;
|
||||
case "BSGN": birthSigns.load(); break;
|
||||
case "LTEX": landTextures.load(); break;
|
||||
case "MGEF": effects.load(); break;
|
||||
case "WEAP": weapons.load(); break;
|
||||
case "REPA": repairs.load(); break;
|
||||
case "LOCK": lockpicks.load(); break;
|
||||
case "PROB": probes.load(); break;
|
||||
case "CELL": cells.load(); break;
|
||||
case "REGN": regions.load(); break;
|
||||
case "LIGH": lights.load(); break;
|
||||
case "SKIL": skills.load(); break;
|
||||
case "SNDG": soundGens.load(); break;
|
||||
case "RACE": races.load(); break;
|
||||
case "MISC": miscItems.load(); break;
|
||||
case "CLOT": clothes.load(); break;
|
||||
case "INGR": ingreds.load(); break;
|
||||
case "CLAS": classes.load(); break;
|
||||
case "CONT": containers.load(); break;
|
||||
case "CREA": creatures.load(); break;
|
||||
case "LEVI": itemLists.load(); break;
|
||||
case "LEVC": creatureLists.load(); break;
|
||||
case "NPC_": npcs.load(); break;
|
||||
case "DIAL": dialogues.load(); break;
|
||||
case "SSCR": startScripts.load(); break;
|
||||
/*
|
||||
|
||||
// Tribunal / Bloodmoon only
|
||||
case "SSCR": loadSSCR.load(); break;
|
||||
|
||||
*/
|
||||
// For save games:
|
||||
// case "NPCC": loadNPCC;
|
||||
// case "CNTC": loadCNTC;
|
||||
// case "CREC": loadCREC;
|
||||
|
||||
// These should never be looked up
|
||||
case "TES3":
|
||||
case "INFO":
|
||||
case "LAND":
|
||||
case "PGRD":
|
||||
esFile.fail("Misplaced record " ~ recName);
|
||||
default:
|
||||
esFile.fail("Unknown record type " ~ recName);
|
||||
}
|
||||
//*/
|
||||
}
|
||||
|
||||
// Um, this has to be in this file for some reason.
|
||||
ListID!(Dialogue) dialogues;
|
||||
|
||||
struct ItemT
|
||||
{
|
||||
Item item;
|
||||
ItemBase *i;
|
||||
|
||||
T* getType(T, ItemType Type)()
|
||||
{
|
||||
return item.getType!(T, Type)();
|
||||
}
|
||||
|
||||
alias getType!(Potion, ItemType.Potion) getPotion;
|
||||
alias getType!(Apparatus, ItemType.Apparatus) getApparatus;
|
||||
alias getType!(Armor, ItemType.Armor) getArmor;
|
||||
alias getType!(Weapon, ItemType.Weapon) getWeapon;
|
||||
alias getType!(Book, ItemType.Book) getBook;
|
||||
alias getType!(Clothing, ItemType.Clothing) getClothing;
|
||||
alias getType!(Light, ItemType.Light) getLight;
|
||||
alias getType!(Ingredient, ItemType.Ingredient) getIngredient;
|
||||
alias getType!(Tool, ItemType.Pick) getPick;
|
||||
alias getType!(Tool, ItemType.Probe) getProbe;
|
||||
alias getType!(Tool, ItemType.Repair) getRepair;
|
||||
alias getType!(Misc, ItemType.Misc) getMisc;
|
||||
alias getType!(LeveledItems, ItemType.ItemList) getItemList;
|
||||
alias getType!(Creature, ItemType.Creature) getCreature;
|
||||
alias getType!(LeveledCreatures, ItemType.CreatureList) getCreatureList;
|
||||
alias getType!(NPC, ItemType.NPC) getNPC;
|
||||
alias getType!(Door, ItemType.Door) getDoor;
|
||||
alias getType!(Activator, ItemType.Activator) getActivator;
|
||||
alias getType!(Static, ItemType.Static) getStatic;
|
||||
alias getType!(Container, ItemType.Container) getContainer;
|
||||
|
||||
static ItemT opCall(Item it)
|
||||
{
|
||||
ItemT itm;
|
||||
itm.item = it;
|
||||
itm.i = it.i;
|
||||
return itm;
|
||||
}
|
||||
}
|
||||
|
||||
void endFiles()
|
||||
{
|
||||
foreach(ListKeeper l; recordLists)
|
||||
l.endFile();
|
||||
|
||||
items.endFile();
|
||||
}
|
||||
|
||||
void initializeLists()
|
||||
{
|
||||
recordLists = null;
|
||||
|
||||
// Initialize all the lists here. The sizes have been chosen big
|
||||
// enough to hold the main ESM files and a large number of mods
|
||||
// without rehashing.
|
||||
|
||||
activators = new ListID!(Activator)(1400);
|
||||
doors = new ListID!(Door)(300);
|
||||
globals = new ListID!(Global)(300);
|
||||
scripts = new ScriptList(1800);
|
||||
sounds = new ListID!(Sound)(1000);
|
||||
gameSettings = new ListID!(GameSetting)(1600);
|
||||
factions = new ListID!(Faction)(30);
|
||||
statics = new ListID!(Static)(4000);
|
||||
spells = new ListID!(Spell)(1300);
|
||||
potions = new ListID!(Potion)(300);
|
||||
appas = new ListID!(Apparatus)(30);
|
||||
armors = new ListID!(Armor)(500);
|
||||
bodyParts = new ListID!(BodyPart)(2300);
|
||||
enchants = new ListID!(Enchantment)(1000);
|
||||
books = new ListID!(Book)(700);
|
||||
birthSigns = new ListID!(BirthSign)(30);
|
||||
landTextures = new LandTextureList;
|
||||
effects = new MagicEffectList;
|
||||
weapons = new ListID!(Weapon)(700);
|
||||
lockpicks = new ListID!(Tool)(10);
|
||||
probes = new ListID!(Tool)(10);
|
||||
repairs = new ListID!(Tool)(10);
|
||||
cells = new CellList;
|
||||
regions = new ListID!(Region)(20);
|
||||
lights = new ListID!(Light)(1000);
|
||||
skills = new SkillList;
|
||||
soundGens = new ListID!(SoundGenerator)(500);
|
||||
races = new ListID!(Race)(100);
|
||||
miscItems = new ListID!(Misc)(700);
|
||||
clothes = new ListID!(Clothing)(700);
|
||||
ingreds = new ListID!(Ingredient)(200);
|
||||
classes = new ListID!(Class)(100);
|
||||
containers = new ListID!(Container)(1200);
|
||||
creatures = new ListID!(Creature)(800);
|
||||
itemLists = new ListID!(LeveledItems)(600);
|
||||
creatureLists = new ListID!(LeveledCreatures)(400);
|
||||
npcs = new ListID!(NPC)(3500);
|
||||
dialogues = new ListID!(Dialogue)(3000);
|
||||
startScripts.init();
|
||||
|
||||
hyperlinks.rehash(1600);
|
||||
|
||||
items.rehash(5500);
|
||||
actors.rehash(5000);
|
||||
cellRefs.rehash(17000);
|
||||
}
|
@ -1,377 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (esmtool.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 esmtool;
|
||||
|
||||
import std.stdio;
|
||||
|
||||
import core.memory;
|
||||
import esm.esmmain;
|
||||
import monster.util.string;
|
||||
import mscripts.setup;
|
||||
|
||||
import std.gc;
|
||||
import gcstats;
|
||||
|
||||
|
||||
// Not used, but we have to link it in along with the C++ stuff.
|
||||
import input.events;
|
||||
|
||||
void poolSize()
|
||||
{
|
||||
GCStats gc;
|
||||
getStats(gc);
|
||||
writefln("Pool size: ", comma(gc.poolsize));
|
||||
writefln("Used size: ", comma(gc.usedsize));
|
||||
}
|
||||
|
||||
//alias int[Dialogue*] TopicList;
|
||||
|
||||
void main(char[][] args)
|
||||
{
|
||||
char[][] files;
|
||||
bool raw;
|
||||
|
||||
bool scptList; // List scripts
|
||||
bool scptShow; // Show a script
|
||||
char[] scptName; // Script to show
|
||||
|
||||
bool ciList; // List interior cells
|
||||
bool ceList; // List exterior cells that have names
|
||||
|
||||
bool weList; // List weapons
|
||||
|
||||
bool gmst; // List game settings
|
||||
|
||||
bool numbers; // List how many there are of each record type
|
||||
|
||||
foreach(char[] a; args[1..$])
|
||||
if(a == "-r") raw = true;
|
||||
|
||||
else if(a == "-sl") scptList = true;
|
||||
else if(a == "-s") scptShow = true;
|
||||
else if(scptShow && scptName == "") scptName = a;
|
||||
|
||||
else if(a == "-g") gmst = true;
|
||||
else if(a == "-cil") ciList = true;
|
||||
else if(a == "-cel") ceList = true;
|
||||
|
||||
else if(a == "-wl") weList = true;
|
||||
|
||||
else if(a == "-n") numbers = true;
|
||||
|
||||
else if(a.begins("-")) writefln("Ignoring unknown option %s", a);
|
||||
else files ~= a;
|
||||
|
||||
int help(char[] msg)
|
||||
{
|
||||
writefln("%s", msg);
|
||||
writefln("Syntax: %s [options] esm-file [esm-file ... ]", args[0]);
|
||||
writefln(" Options:");
|
||||
writefln(" -r Display all records in raw format");
|
||||
writefln(" -n List the number of each record type");
|
||||
writefln(" -sl List scripts");
|
||||
writefln(" -g List game settings (GMST)");
|
||||
writefln(" -s name Show given script");
|
||||
writefln(" -cil List interior cells");
|
||||
writefln(" -cel List exterior cells with names");
|
||||
writefln(" -wl List weapons");
|
||||
return 1;
|
||||
}
|
||||
if(files.length == 0) return help("No input files given");
|
||||
|
||||
if(scptShow && scptName == "") return help("No script name given");
|
||||
|
||||
initializeMemoryRegions();
|
||||
initMonsterScripts();
|
||||
|
||||
if(raw)
|
||||
{
|
||||
foreach(int fileNum, char[] filename; files)
|
||||
{
|
||||
try
|
||||
{
|
||||
esFile.open(filename, esmRegion);
|
||||
printRaw();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
try {writefln(e);}
|
||||
catch {}
|
||||
writefln("Error on file %s", filename);
|
||||
}
|
||||
catch
|
||||
{
|
||||
writefln("Error: Unkown failure on file %s", filename);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable resource lookups.
|
||||
resources.dummy = true;
|
||||
|
||||
try loadTESFiles(files);
|
||||
catch(Exception e)
|
||||
{
|
||||
writefln(e);
|
||||
}
|
||||
catch { writefln("Error: Unkown failure"); }
|
||||
|
||||
// List weapons
|
||||
if(weList) foreach(n, m; weapons.names)
|
||||
{
|
||||
alias Weapon.Type WT;
|
||||
switch(m.data.type)
|
||||
{
|
||||
case WT.ShortBladeOneHand: writef("Short Sword"); break;
|
||||
case WT.LongBladeOneHand: writef("Long Sword, One-Handed"); break;
|
||||
case WT.LongBladeTwoHand: writef("Long Sword, Two-Handed"); break;
|
||||
case WT.BluntOneHand: writef("Blunt, One-Handed"); break;
|
||||
case WT.BluntTwoClose: writef("Blunt, Two-Handed"); break;
|
||||
case WT.BluntTwoWide: writef("Blunt, Two-Handed Wide"); break;
|
||||
case WT.SpearTwoWide: writef("Spear, Two-Handed"); break;
|
||||
case WT.AxeOneHand: writef("Axe, One-Handed"); break;
|
||||
case WT.AxeTwoHand: writef("Axe, Two-Handed"); break;
|
||||
case WT.MarksmanBow: writef("Bow"); break;
|
||||
case WT.MarksmanCrossbow: writef("Crossbow"); break;
|
||||
case WT.MarksmanThrown: writef("Thrown weapon"); break;
|
||||
case WT.Arrow: writef("Arrow"); break;
|
||||
case WT.Bolt: writef("Bolt"); break;
|
||||
default: assert(0);
|
||||
}
|
||||
writefln(" id '%s': name '%s'", n, m.name);
|
||||
|
||||
if(m.data.flags & Weapon.Flags.Magical)
|
||||
writefln("Magical");
|
||||
if(m.data.flags & Weapon.Flags.Silver)
|
||||
writefln("Silver");
|
||||
|
||||
writefln("Weight: ", m.data.weight);
|
||||
writefln("Value: ", m.data.value);
|
||||
writefln("Health: ", m.data.health);
|
||||
writefln("Speed: ", m.data.speed);
|
||||
writefln("Reach: ", m.data.reach);
|
||||
writefln("Enchantment points: ", m.data.enchant);
|
||||
writefln("Combat: ", m.data.chop, m.data.slash, m.data.thrust);
|
||||
|
||||
if(m.enchant) writefln("Has enchantment '%s'", m.enchant.id);
|
||||
if(m.script) writefln("Has script '%s'", m.script.id);
|
||||
|
||||
writefln();
|
||||
}
|
||||
|
||||
if(numbers)
|
||||
{
|
||||
writefln("Activators: ", activators.length);
|
||||
writefln("Doors: ", doors.length);
|
||||
writefln("Globals: ", globals.length);
|
||||
writefln("Sounds: ", sounds.length);
|
||||
writefln("Game Settings: ", gameSettings.length);
|
||||
writefln("Factions: ", factions.length);
|
||||
writefln("Statics: ", statics.length);
|
||||
writefln("Spells: ", spells.length);
|
||||
writefln("Potions: ", potions.length);
|
||||
writefln("Apparatus: ", appas.length);
|
||||
writefln("Armors: ", armors.length);
|
||||
writefln("Body parts: ", bodyParts.length);
|
||||
writefln("Enchantments: ", enchants.length);
|
||||
writefln("Books: ", books.length);
|
||||
writefln("Birth signs: ", birthSigns.length);
|
||||
writefln("Land texture files: ", landTextures.length);
|
||||
writefln("Weapons: ", weapons.length);
|
||||
writefln("Lockpicks: ", lockpicks.length);
|
||||
writefln("Probes: ", probes.length);
|
||||
writefln("Repairs: ", repairs.length);
|
||||
writefln("Cells: ", cells.length);
|
||||
writefln(" Interior: ", cells.numInt);
|
||||
writefln(" Exterior: ", cells.numExt);
|
||||
writefln("Regions: ", regions.length);
|
||||
writefln("Lights: ", lights.length);
|
||||
writefln("Skills: ", skills.length);
|
||||
writefln("Sound generators: ", soundGens.length);
|
||||
writefln("Races: ", races.length);
|
||||
writefln("Misc items: ", miscItems.length);
|
||||
writefln("Cloths: ", clothes.length);
|
||||
writefln("Ingredients: ", ingreds.length);
|
||||
writefln("Classes: ", classes.length);
|
||||
writefln("Containers: ", containers.length);
|
||||
writefln("Creatures: ", creatures.length);
|
||||
writefln("Leveled item lists: ", itemLists.length);
|
||||
writefln("Leveled creature lists: ", creatureLists.length);
|
||||
writefln("NPCs: ", npcs.length);
|
||||
writefln("Scripts: ", scripts.length);
|
||||
writefln("Dialogues: ", dialogues.length);
|
||||
writefln("Hyperlinks: ", hyperlinks.list.length);
|
||||
writefln("Start scripts: ", startScripts.length);
|
||||
writefln("\nTotal items: ", items.length);
|
||||
writefln("Total actors: ", actors.length);
|
||||
writefln("Total cell placable items: ", cellRefs.length);
|
||||
}
|
||||
if(gmst)
|
||||
{
|
||||
foreach(a, b; gameSettings.names)
|
||||
{
|
||||
writef(a, " (");
|
||||
if(b.type == VarType.Int) writefln("int) = ", b.i);
|
||||
else if(b.type == VarType.Float) writefln("float) = ", b.f);
|
||||
else if(b.type == VarType.String) writefln("string) = '%s'", b.str);
|
||||
else writefln("no value)", cast(int)b.type);
|
||||
}
|
||||
}
|
||||
|
||||
if(scptList) foreach(a, b; scripts.names) writefln(a);
|
||||
if(ciList)
|
||||
foreach(a, b; cells.in_cells)
|
||||
writefln(a);
|
||||
if(ceList)
|
||||
foreach(uint i, c; .cells.ex_cells)
|
||||
{
|
||||
int x, y;
|
||||
CellList.decompound(i, x, y);
|
||||
if(c.name.length)
|
||||
writefln("%s,%s: %s", x, y, c.name);
|
||||
}
|
||||
|
||||
if(scptShow)
|
||||
{
|
||||
Script *p = scripts.names.lookup(scptName);
|
||||
if(p)
|
||||
writefln("Script '%s', text is:\n-------\n%s\n-------", p.id, p.scriptText);
|
||||
else writefln("Script '%s' not found", scptName);
|
||||
writefln();
|
||||
}
|
||||
|
||||
writefln(esmRegion);
|
||||
|
||||
poolSize();
|
||||
}
|
||||
|
||||
// Quick function that simply iterates through an ES file and prints
|
||||
// out all the records and subrecords. Some of this code is really old
|
||||
// (about 2004-2005)
|
||||
void printRaw()
|
||||
{
|
||||
with(esFile)
|
||||
{
|
||||
// Variable length integer (this only works for unsigned ints!)
|
||||
ulong getHVUint()
|
||||
{
|
||||
ulong l;
|
||||
|
||||
getSubHeader();
|
||||
if( (getSubSize != 4) &&
|
||||
(getSubSize != 2) &&
|
||||
(getSubSize != 8) )
|
||||
fail(format("Unknown integer size: ", getSubSize));
|
||||
|
||||
readExact(&l, getSubSize);
|
||||
return l;
|
||||
}
|
||||
|
||||
writefln("Filename: ", getFilename);
|
||||
writef("Filetype: ");
|
||||
switch(getFileType())
|
||||
{
|
||||
case FileType.Plugin: writefln("Plugin"); break;
|
||||
case FileType.Master: writefln("Master"); break;
|
||||
case FileType.Savegame: writefln("Savegame"); break;
|
||||
case FileType.Unknown: writefln("Unknown"); break;
|
||||
default: assert(0);
|
||||
}
|
||||
writef("Version: ");
|
||||
if(isVer12()) writefln("1.2");
|
||||
else if(isVer13()) writefln("1.3");
|
||||
else writefln("Unknown");
|
||||
|
||||
writefln("Records: ", getRecords);
|
||||
writefln("Master files:");
|
||||
for(int i; i<getMasters.length; i++)
|
||||
writefln(" %s", getMasters[i].name, ", ", getMasters[i].size, " bytes");
|
||||
|
||||
writefln("Author: %s", getAuthor);
|
||||
//writefln("Description: %s", desc);
|
||||
writefln("Total file size: %d\n", getFileSize);
|
||||
|
||||
writefln("List of records:");
|
||||
|
||||
while(hasMoreRecs())
|
||||
{
|
||||
uint flags;
|
||||
|
||||
// Read record header
|
||||
char[] recName = getRecName();
|
||||
getRecHeader(flags);
|
||||
|
||||
if(flags)
|
||||
{
|
||||
writef("Flags: ");
|
||||
if(flags & RecordFlags.Persistent) writef("Persistent ");
|
||||
if(flags & RecordFlags.Blocked) writef("Blocked ");
|
||||
if(flags & RecordFlags.Flag6) writef("Flag6 ");
|
||||
if(flags & RecordFlags.Flag13) writef("Flag13 ");
|
||||
writefln();
|
||||
if(flags & RecordFlags.Unknown)
|
||||
writefln("UNKNOWN flags are set: %xh", flags);
|
||||
}
|
||||
|
||||
// Process sub record
|
||||
writef("%s %d bytes", recName, getRecLeft());
|
||||
writefln();
|
||||
while(hasMoreSubs())
|
||||
{
|
||||
getSubName();
|
||||
char[] subName = retSubName();
|
||||
writef(" %s = ", subName);
|
||||
|
||||
// Process header
|
||||
if(subName == "NAME" || subName == "STRV" ||
|
||||
subName == "FNAM" || subName == "MODL" ||
|
||||
subName == "SCRI" || subName == "RGNN" ||
|
||||
subName == "BNAM" || subName == "ONAM" ||
|
||||
subName == "INAM" || subName == "SCVR" ||
|
||||
subName == "RNAM" || subName == "DNAM" ||
|
||||
subName == "ANAM")
|
||||
//subName == "SCTX") // For script text
|
||||
//getHString();
|
||||
{
|
||||
writefln("'%s'", getHString());
|
||||
}
|
||||
else if(subName == "FLTV" || subName == "XSCL")
|
||||
{
|
||||
float f = getHFloat();
|
||||
writefln("f=", f, " i=", *(cast(int*)&f));
|
||||
}
|
||||
else if(subName == "INTV" /*|| subName == "NAM0"*/ || subName == "FRMR")
|
||||
writefln(getHVUint());
|
||||
else
|
||||
{
|
||||
int left = skipHSub();
|
||||
writefln(left, " bytes");
|
||||
}
|
||||
}
|
||||
writefln();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,336 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (events.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 input.events;
|
||||
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
|
||||
import sound.audio;
|
||||
|
||||
import core.config;
|
||||
|
||||
import scene.soundlist;
|
||||
import scene.player;
|
||||
|
||||
import bullet.bindings;
|
||||
|
||||
import monster.monster;
|
||||
import monster.vm.dbg;
|
||||
|
||||
import ogre.bindings;
|
||||
import gui.bindings;
|
||||
import gui.gui;
|
||||
|
||||
import input.keys;
|
||||
import input.ois;
|
||||
|
||||
// Debug output
|
||||
//debug=printMouse; // Mouse button events
|
||||
//debug=printMouseMove; // Mouse movement events
|
||||
//debug=printKeys; // Keypress events
|
||||
|
||||
// TODO: Jukebox controls and other state-related data will later be
|
||||
// handled entirely in script code, as will some of the key bindings.
|
||||
|
||||
// Pause?
|
||||
bool pause = false;
|
||||
int *guiMode;
|
||||
|
||||
const float volDiff = 0.05;
|
||||
|
||||
void musVolume(bool increase)
|
||||
{
|
||||
float diff = -volDiff;
|
||||
if(increase) diff = -diff;
|
||||
config.setMusicVolume(diff + config.getMusicVolume);
|
||||
writefln(increase?"Increasing":"Decreasing", " music volume to ", config.getMusicVolume);
|
||||
}
|
||||
|
||||
void sfxVolume(bool increase)
|
||||
{
|
||||
float diff = -volDiff;
|
||||
if(increase) diff = -diff;
|
||||
config.setSfxVolume(diff + config.getSfxVolume);
|
||||
writefln(increase?"Increasing":"Decreasing", " sound effect volume to ", config.getSfxVolume);
|
||||
}
|
||||
|
||||
void mainVolume(bool increase)
|
||||
{
|
||||
float diff = -volDiff;
|
||||
if(increase) diff = -diff;
|
||||
config.setMainVolume(diff + config.getMainVolume);
|
||||
writefln(increase?"Increasing":"Decreasing", " main volume to ", config.getMainVolume);
|
||||
}
|
||||
|
||||
void updateMouseSensitivity()
|
||||
{
|
||||
effMX = *config.mouseSensX;
|
||||
effMY = *config.mouseSensY;
|
||||
if(*config.flipMouseY) effMY = -effMY;
|
||||
}
|
||||
|
||||
void togglePause()
|
||||
{
|
||||
pause = !pause;
|
||||
if(pause) writefln("Pause");
|
||||
else writefln("Pause off");
|
||||
}
|
||||
|
||||
extern(C) void d_handleMouseButton(MouseState *state, int button)
|
||||
{
|
||||
debug(printMouse)
|
||||
writefln("handleMouseButton %s: Abs(%s, %s, %s)", button,
|
||||
state.X.abs, state.Y.abs, state.Z.abs);
|
||||
|
||||
// For the moment, just treat mouse clicks as normal key presses.
|
||||
d_handleKey(cast(KC) (KC.Mouse0 + button));
|
||||
}
|
||||
|
||||
// Handle a keyboard event through key bindings. Direct movement
|
||||
// (eg. arrow keys) is not handled here, see d_frameStarted() below.
|
||||
extern(C) void d_handleKey(KC keycode, dchar text = 0)
|
||||
{
|
||||
// Do some preprocessing on the data to account for OIS
|
||||
// shortcommings.
|
||||
|
||||
// Some keys (especially international keys) have no key code but
|
||||
// return a character instead.
|
||||
if(keycode == 0)
|
||||
{
|
||||
// If no character is given, just drop this event since OIS did
|
||||
// not manage to give us any useful data at all.
|
||||
if(text == 0) return;
|
||||
|
||||
keycode = KC.CharOnly;
|
||||
}
|
||||
|
||||
// Debug output
|
||||
debug(printKeys)
|
||||
{
|
||||
char[] str;
|
||||
if(keycode >= 0 && keycode < keysymToString.length)
|
||||
str = keysymToString[keycode];
|
||||
else str = "OUT_OF_RANGE";
|
||||
writefln("Key %s, text '%s', name '%s'", keycode, text, str);
|
||||
}
|
||||
|
||||
// Look up the key binding. We have to send both the keycode and the
|
||||
// text.
|
||||
Keys k = keyBindings.findMatch(keycode, text);
|
||||
|
||||
// These are handled even if we are in gui mode:
|
||||
if(k)
|
||||
switch(k)
|
||||
{
|
||||
case Keys.ToggleGui: gui_toggleGui(); return;
|
||||
case Keys.Console: gui_toggleConsole(); return;
|
||||
case Keys.ScreenShot: takeScreenShot(); return;
|
||||
default:
|
||||
}
|
||||
|
||||
if(*guiMode) return;
|
||||
|
||||
if(k)
|
||||
switch(k)
|
||||
{
|
||||
case Keys.ToggleBattleMusic:
|
||||
Music.toggle();
|
||||
return;
|
||||
|
||||
case Keys.MainVolUp: mainVolume(true); return;
|
||||
case Keys.MainVolDown: mainVolume(false); return;
|
||||
case Keys.MusVolUp: musVolume(true); return;
|
||||
case Keys.MusVolDown: musVolume(false); return;
|
||||
case Keys.SfxVolUp: sfxVolume(true); return;
|
||||
case Keys.SfxVolDown: sfxVolume(false); return;
|
||||
case Keys.Mute: Music.toggleMute(); return;
|
||||
case Keys.Fullscreen: toggleFullscreen(); return;
|
||||
|
||||
case Keys.PhysMode: bullet_nextMode(); return;
|
||||
case Keys.Nighteye: ogre_toggleLight(); return;
|
||||
|
||||
case Keys.Debug: return;
|
||||
case Keys.Pause: togglePause(); return;
|
||||
case Keys.Exit: exitProgram(); return;
|
||||
default:
|
||||
assert(k >= 0 && k < keyToString.length);
|
||||
writefln("WARNING: Event %s has no effect", keyToString[k]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh rate for sfx placements, in seconds.
|
||||
const float sndRefresh = 0.17;
|
||||
|
||||
// Refresh rate for music fadeing, seconds.
|
||||
const float musRefresh = 0.05;
|
||||
|
||||
// Walking / floating speed, in points per second.
|
||||
float speed = 300;
|
||||
|
||||
float sndCumTime = 0;
|
||||
float musCumTime = 0;
|
||||
|
||||
// Move the player according to playerData.position
|
||||
void movePlayer()
|
||||
{
|
||||
// Move the player into place. TODO: This isn't really input-related
|
||||
// at all, and should be moved.
|
||||
with(*playerData.position)
|
||||
{
|
||||
ogre_moveCamera(position[0], position[1], position[2]);
|
||||
ogre_setCameraRotation(rotation[0], rotation[1], rotation[2]);
|
||||
|
||||
bullet_movePlayer(position[0], position[1], position[2]);
|
||||
}
|
||||
}
|
||||
|
||||
void initializeInput()
|
||||
{
|
||||
movePlayer();
|
||||
|
||||
// TODO/FIXME: This should have been in config, but DMD's module
|
||||
// system is on the brink of collapsing, and it won't compile if I
|
||||
// put another import in core.config. I should probably check the
|
||||
// bug list and report it.
|
||||
updateMouseSensitivity();
|
||||
|
||||
// Get a pointer to the 'guiMode' flag in cpp_ogre.cpp
|
||||
guiMode = gui_getGuiModePtr();
|
||||
}
|
||||
|
||||
extern(C) int ois_isPressed(int keysym);
|
||||
|
||||
// Check if a key is currently down
|
||||
bool isPressed(Keys key)
|
||||
{
|
||||
KeyBind *b = &keyBindings.bindings[key];
|
||||
foreach(i; b.syms)
|
||||
if(i != 0 && ois_isPressed(i)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable superman mode, ie. flight and super-speed. Only used for
|
||||
// debugging the terrain mode.
|
||||
extern(C) void d_terr_superman()
|
||||
{
|
||||
bullet_fly();
|
||||
speed = 8000;
|
||||
|
||||
with(*playerData.position)
|
||||
{
|
||||
position[0] = 20000;
|
||||
position[1] = -70000;
|
||||
position[2] = 30000;
|
||||
}
|
||||
movePlayer();
|
||||
}
|
||||
|
||||
extern(C) int d_frameStarted(float time)
|
||||
{
|
||||
if(doExit) return 0;
|
||||
|
||||
dbg.trace("d_frameStarted");
|
||||
scope(exit) dbg.untrace();
|
||||
|
||||
// Run the Monster scheduler
|
||||
vm.frame(time);
|
||||
|
||||
musCumTime += time;
|
||||
if(musCumTime > musRefresh)
|
||||
{
|
||||
Music.updateBuffers();
|
||||
musCumTime -= musRefresh;
|
||||
}
|
||||
|
||||
// The rest is ignored in pause or GUI mode
|
||||
if(pause || *guiMode > 0) return 1;
|
||||
|
||||
// Check if the movement keys are pressed
|
||||
float moveX = 0, moveY = 0, moveZ = 0;
|
||||
float x, y, z, ox, oy, oz;
|
||||
|
||||
if(isPressed(Keys.MoveLeft)) moveX -= speed;
|
||||
if(isPressed(Keys.MoveRight)) moveX += speed;
|
||||
if(isPressed(Keys.MoveForward)) moveZ -= speed;
|
||||
if(isPressed(Keys.MoveBackward)) moveZ += speed;
|
||||
|
||||
// TODO: These should be enabled for floating modes (like swimming
|
||||
// and levitation) and disabled for everything else.
|
||||
if(isPressed(Keys.MoveUp)) moveY += speed;
|
||||
if(isPressed(Keys.MoveDown)) moveY -= speed;
|
||||
|
||||
// This isn't very elegant, but it's simple and it works.
|
||||
|
||||
// Get the current coordinates
|
||||
ogre_getCameraPos(&ox, &oy, &oz);
|
||||
|
||||
// Move camera using relative coordinates. TODO: We won't really
|
||||
// need to move the camera here (since it's moved below anyway), we
|
||||
// only want the transformation from camera space to world
|
||||
// space. This can likely be done more efficiently.
|
||||
ogre_moveCameraRel(moveX, moveY, moveZ);
|
||||
|
||||
// Get the result
|
||||
ogre_getCameraPos(&x, &y, &z);
|
||||
|
||||
// The result is the real movement direction, in world coordinates
|
||||
moveX = x-ox;
|
||||
moveY = y-oy;
|
||||
moveZ = z-oz;
|
||||
|
||||
// Tell Bullet that this is where we want to go
|
||||
bullet_setPlayerDir(moveX, moveY, moveZ);
|
||||
|
||||
// Perform a Bullet time step
|
||||
bullet_timeStep(time);
|
||||
|
||||
// Get the final (actual) player position and update the camera
|
||||
bullet_getPlayerPos(&x, &y, &z);
|
||||
ogre_moveCamera(x,y,z);
|
||||
|
||||
// Store it in the player object
|
||||
playerData.position.position[0] = x;
|
||||
playerData.position.position[1] = y;
|
||||
playerData.position.position[2] = z;
|
||||
|
||||
if(!config.noSound)
|
||||
{
|
||||
// Tell the sound scene that the player has moved
|
||||
sndCumTime += time;
|
||||
if(sndCumTime > sndRefresh)
|
||||
{
|
||||
float fx, fy, fz;
|
||||
float ux, uy, uz;
|
||||
|
||||
ogre_getCameraOrientation(&fx, &fy, &fz, &ux, &uy, &uz);
|
||||
|
||||
soundScene.update(x,y,z,fx,fy,fz,ux,uy,uz);
|
||||
sndCumTime -= sndRefresh;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool collides = false;
|
@ -1,428 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (ois.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 input.ois;
|
||||
|
||||
// Mouse buttons
|
||||
enum MB : int
|
||||
{
|
||||
Button0 = 0,
|
||||
Left = Button0,
|
||||
Button1 = 1,
|
||||
Right = Button1,
|
||||
Button2 = 2,
|
||||
Middle = Button2,
|
||||
|
||||
Button3 = 3,
|
||||
Button4 = 4,
|
||||
Button5 = 5,
|
||||
Button6 = 6,
|
||||
Button7 = 7,
|
||||
|
||||
LastMouse
|
||||
}
|
||||
|
||||
// Keyboard scan codes
|
||||
enum KC : int
|
||||
{
|
||||
UNASSIGNED = 0x00,
|
||||
ESCAPE = 0x01,
|
||||
N1 = 0x02,
|
||||
N2 = 0x03,
|
||||
N3 = 0x04,
|
||||
N4 = 0x05,
|
||||
N5 = 0x06,
|
||||
N6 = 0x07,
|
||||
N7 = 0x08,
|
||||
N8 = 0x09,
|
||||
N9 = 0x0A,
|
||||
N0 = 0x0B,
|
||||
MINUS = 0x0C, // - on main keyboard
|
||||
EQUALS = 0x0D,
|
||||
BACK = 0x0E, // backspace
|
||||
TAB = 0x0F,
|
||||
Q = 0x10,
|
||||
W = 0x11,
|
||||
E = 0x12,
|
||||
R = 0x13,
|
||||
T = 0x14,
|
||||
Y = 0x15,
|
||||
U = 0x16,
|
||||
I = 0x17,
|
||||
O = 0x18,
|
||||
P = 0x19,
|
||||
LBRACKET = 0x1A,
|
||||
RBRACKET = 0x1B,
|
||||
RETURN = 0x1C, // Enter on main keyboard
|
||||
LCONTROL = 0x1D,
|
||||
A = 0x1E,
|
||||
S = 0x1F,
|
||||
D = 0x20,
|
||||
F = 0x21,
|
||||
G = 0x22,
|
||||
H = 0x23,
|
||||
J = 0x24,
|
||||
K = 0x25,
|
||||
L = 0x26,
|
||||
SEMICOLON = 0x27,
|
||||
APOSTROPHE = 0x28,
|
||||
GRAVE = 0x29, // accent
|
||||
LSHIFT = 0x2A,
|
||||
BACKSLASH = 0x2B,
|
||||
Z = 0x2C,
|
||||
X = 0x2D,
|
||||
C = 0x2E,
|
||||
V = 0x2F,
|
||||
B = 0x30,
|
||||
N = 0x31,
|
||||
M = 0x32,
|
||||
COMMA = 0x33,
|
||||
PERIOD = 0x34, // . on main keyboard
|
||||
SLASH = 0x35, // / on main keyboard
|
||||
RSHIFT = 0x36,
|
||||
MULTIPLY = 0x37, // * on numeric keypad
|
||||
LMENU = 0x38, // left Alt
|
||||
SPACE = 0x39,
|
||||
CAPITAL = 0x3A,
|
||||
F1 = 0x3B,
|
||||
F2 = 0x3C,
|
||||
F3 = 0x3D,
|
||||
F4 = 0x3E,
|
||||
F5 = 0x3F,
|
||||
F6 = 0x40,
|
||||
F7 = 0x41,
|
||||
F8 = 0x42,
|
||||
F9 = 0x43,
|
||||
F10 = 0x44,
|
||||
NUMLOCK = 0x45,
|
||||
SCROLL = 0x46, // Scroll Lock
|
||||
NUMPAD7 = 0x47,
|
||||
NUMPAD8 = 0x48,
|
||||
NUMPAD9 = 0x49,
|
||||
SUBTRACT = 0x4A, // - on numeric keypad
|
||||
NUMPAD4 = 0x4B,
|
||||
NUMPAD5 = 0x4C,
|
||||
NUMPAD6 = 0x4D,
|
||||
ADD = 0x4E, // + on numeric keypad
|
||||
NUMPAD1 = 0x4F,
|
||||
NUMPAD2 = 0x50,
|
||||
NUMPAD3 = 0x51,
|
||||
NUMPAD0 = 0x52,
|
||||
DECIMAL = 0x53, // . on numeric keypad
|
||||
OEM_102 = 0x56, // < > | on UK/Germany keyboards
|
||||
F11 = 0x57,
|
||||
F12 = 0x58,
|
||||
F13 = 0x64, // (NEC PC98)
|
||||
F14 = 0x65, // (NEC PC98)
|
||||
F15 = 0x66, // (NEC PC98)
|
||||
KANA = 0x70, // (Japanese keyboard)
|
||||
ABNT_C1 = 0x73, // / ? on Portugese (Brazilian) keyboards
|
||||
CONVERT = 0x79, // (Japanese keyboard)
|
||||
NOCONVERT = 0x7B, // (Japanese keyboard)
|
||||
YEN = 0x7D, // (Japanese keyboard)
|
||||
ABNT_C2 = 0x7E, // Numpad . on Portugese (Brazilian) keyboards
|
||||
NUMPADEQUALS= 0x8D, // = on numeric keypad (NEC PC98)
|
||||
PREVTRACK = 0x90, // Previous Track (CIRCUMFLEX on Japanese keyboard)
|
||||
AT = 0x91, // (NEC PC98)
|
||||
COLON = 0x92, // (NEC PC98)
|
||||
UNDERLINE = 0x93, // (NEC PC98)
|
||||
KANJI = 0x94, // (Japanese keyboard)
|
||||
STOP = 0x95, // (NEC PC98)
|
||||
AX = 0x96, // (Japan AX)
|
||||
UNLABELED = 0x97, // (J3100)
|
||||
NEXTTRACK = 0x99, // Next Track
|
||||
NUMPADENTER = 0x9C, // Enter on numeric keypad
|
||||
RCONTROL = 0x9D,
|
||||
MUTE = 0xA0, // Mute
|
||||
CALCULATOR = 0xA1, // Calculator
|
||||
PLAYPAUSE = 0xA2, // Play / Pause
|
||||
MEDIASTOP = 0xA4, // Media Stop
|
||||
VOLUMEDOWN = 0xAE, // Volume -
|
||||
VOLUMEUP = 0xB0, // Volume +
|
||||
WEBHOME = 0xB2, // Web home
|
||||
NUMPADCOMMA = 0xB3, // , on numeric keypad (NEC PC98)
|
||||
DIVIDE = 0xB5, // / on numeric keypad
|
||||
SYSRQ = 0xB7, // Also called print screen
|
||||
RMENU = 0xB8, // right Alt
|
||||
PAUSE = 0xC5, // Pause
|
||||
HOME = 0xC7, // Home on arrow keypad
|
||||
UP = 0xC8, // UpArrow on arrow keypad
|
||||
PGUP = 0xC9, // PgUp on arrow keypad
|
||||
LEFT = 0xCB, // LeftArrow on arrow keypad
|
||||
RIGHT = 0xCD, // RightArrow on arrow keypad
|
||||
END = 0xCF, // End on arrow keypad
|
||||
DOWN = 0xD0, // DownArrow on arrow keypad
|
||||
PGDOWN = 0xD1, // PgDn on arrow keypad
|
||||
INSERT = 0xD2, // Insert on arrow keypad
|
||||
DELETE = 0xD3, // Delete on arrow keypad
|
||||
LWIN = 0xDB, // Left Windows key
|
||||
RWIN = 0xDC, // Right Windows key
|
||||
APPS = 0xDD, // AppMenu key
|
||||
POWER = 0xDE, // System Power
|
||||
SLEEP = 0xDF, // System Sleep
|
||||
WAKE = 0xE3, // System Wake
|
||||
WEBSEARCH = 0xE5, // Web Search
|
||||
WEBFAVORITES= 0xE6, // Web Favorites
|
||||
WEBREFRESH = 0xE7, // Web Refresh
|
||||
WEBSTOP = 0xE8, // Web Stop
|
||||
WEBFORWARD = 0xE9, // Web Forward
|
||||
WEBBACK = 0xEA, // Web Back
|
||||
MYCOMPUTER = 0xEB, // My Computer
|
||||
MAIL = 0xEC, // Mail
|
||||
MEDIASELECT = 0xED, // Media Select
|
||||
|
||||
CharOnly = 0xFF, // Set when the keysym is 0 but the
|
||||
// character is set. This happens with many
|
||||
// international characters or reassigned
|
||||
// characters.
|
||||
|
||||
Mouse0 = 0x100, // Mouse button events can be handled as
|
||||
Mouse1 = 0x101, // keypresses too.
|
||||
Mouse2 = 0x102,
|
||||
Mouse3 = 0x103,
|
||||
Mouse4 = 0x104,
|
||||
Mouse5 = 0x105,
|
||||
Mouse6 = 0x106,
|
||||
Mouse7 = 0x107,
|
||||
}
|
||||
|
||||
// Sigh. I guess we have to do this for Monster at some poing anyway,
|
||||
// so this work isn't completely wasted. Later we can make a generic
|
||||
// conversion between OIS-keysyms, SDL-keysyms and others to the
|
||||
// Monster keysyms. It sucks that everybody tries to reinvent the
|
||||
// wheel as often as they can, but that's the way it goes.
|
||||
|
||||
const char[][] keysymToString =
|
||||
[
|
||||
KC.UNASSIGNED : "UNASSIGNED",
|
||||
KC.ESCAPE : "escape",
|
||||
KC.N1 : "1",
|
||||
KC.N2 : "2",
|
||||
KC.N3 : "3",
|
||||
KC.N4 : "4",
|
||||
KC.N5 : "5",
|
||||
KC.N6 : "6",
|
||||
KC.N7 : "7",
|
||||
KC.N8 : "8",
|
||||
KC.N9 : "9",
|
||||
KC.N0 : "0",
|
||||
KC.MINUS : "minus",
|
||||
KC.EQUALS : "equals",
|
||||
KC.BACK : "backspace",
|
||||
KC.TAB : "tab",
|
||||
KC.Q : "q",
|
||||
KC.W : "w",
|
||||
KC.E : "e",
|
||||
KC.R : "r",
|
||||
KC.T : "t",
|
||||
KC.Y : "y",
|
||||
KC.U : "u",
|
||||
KC.I : "i",
|
||||
KC.O : "o",
|
||||
KC.P : "p",
|
||||
KC.LBRACKET : "{",
|
||||
KC.RBRACKET : "}",
|
||||
KC.RETURN : "enter",
|
||||
KC.LCONTROL : "left_ctrl",
|
||||
KC.A : "a",
|
||||
KC.S : "s",
|
||||
KC.D : "d",
|
||||
KC.F : "f",
|
||||
KC.G : "g",
|
||||
KC.H : "h",
|
||||
KC.J : "j",
|
||||
KC.K : "k",
|
||||
KC.L : "l",
|
||||
KC.SEMICOLON : "semicolon",
|
||||
KC.APOSTROPHE : "apostrophe",
|
||||
KC.GRAVE : "grave",
|
||||
KC.LSHIFT : "left_shift",
|
||||
KC.BACKSLASH : "backslash",
|
||||
KC.Z : "z",
|
||||
KC.X : "x",
|
||||
KC.C : "c",
|
||||
KC.V : "v",
|
||||
KC.B : "b",
|
||||
KC.N : "n",
|
||||
KC.M : "m",
|
||||
KC.COMMA : "comma",
|
||||
KC.PERIOD : "period",
|
||||
KC.SLASH : "slash",
|
||||
KC.RSHIFT : "right_shift",
|
||||
KC.MULTIPLY : "numpad_mult",
|
||||
KC.LMENU : "left_alt",
|
||||
KC.SPACE : "space",
|
||||
KC.CAPITAL : "capital",
|
||||
KC.F1 : "f1",
|
||||
KC.F2 : "f2",
|
||||
KC.F3 : "f3",
|
||||
KC.F4 : "f4",
|
||||
KC.F5 : "f5",
|
||||
KC.F6 : "f6",
|
||||
KC.F7 : "f7",
|
||||
KC.F8 : "f8",
|
||||
KC.F9 : "f9",
|
||||
KC.F10 : "f10",
|
||||
KC.NUMLOCK : "numlock",
|
||||
KC.SCROLL : "scroll",
|
||||
KC.NUMPAD7 : "numpad_7",
|
||||
KC.NUMPAD8 : "numpad_8",
|
||||
KC.NUMPAD9 : "numpad_9",
|
||||
KC.SUBTRACT : "numpad_minus",
|
||||
KC.NUMPAD4 : "numpad_4",
|
||||
KC.NUMPAD5 : "numpad_5",
|
||||
KC.NUMPAD6 : "numpad_6",
|
||||
KC.ADD : "numpad_plus",
|
||||
KC.NUMPAD1 : "numpad_1",
|
||||
KC.NUMPAD2 : "numpad_2",
|
||||
KC.NUMPAD3 : "numpad_3",
|
||||
KC.NUMPAD0 : "numpad_0",
|
||||
KC.DECIMAL : "numpad_period",
|
||||
KC.OEM_102 : "oem102",
|
||||
KC.F11 : "f11",
|
||||
KC.F12 : "f12",
|
||||
KC.F13 : "f13",
|
||||
KC.F14 : "f14",
|
||||
KC.F15 : "f15",
|
||||
KC.KANA : "kana",
|
||||
KC.ABNT_C1 : "abnt_c1",
|
||||
KC.CONVERT : "convert",
|
||||
KC.NOCONVERT : "noconvert",
|
||||
KC.YEN : "yen",
|
||||
KC.ABNT_C2 : "abnt_c2",
|
||||
KC.NUMPADEQUALS: "numpad_equals",
|
||||
KC.PREVTRACK : "prev_track",
|
||||
KC.AT : "at",
|
||||
KC.COLON : "colon",
|
||||
KC.UNDERLINE : "underline",
|
||||
KC.KANJI : "kanji",
|
||||
KC.STOP : "stop",
|
||||
KC.AX : "ax",
|
||||
KC.UNLABELED : "unlabeled",
|
||||
KC.NEXTTRACK : "next_track",
|
||||
KC.NUMPADENTER : "numpad_enter",
|
||||
KC.RCONTROL : "right_control",
|
||||
KC.MUTE : "mute",
|
||||
KC.CALCULATOR : "calculator",
|
||||
KC.PLAYPAUSE : "play_pause",
|
||||
KC.MEDIASTOP : "media_stop",
|
||||
KC.VOLUMEDOWN : "volume_down",
|
||||
KC.VOLUMEUP : "volume_up",
|
||||
KC.WEBHOME : "webhome",
|
||||
KC.NUMPADCOMMA : "numpad_comma",
|
||||
KC.DIVIDE : "numpad_divide",
|
||||
KC.SYSRQ : "print_screen",
|
||||
KC.RMENU : "right_alt",
|
||||
KC.PAUSE : "pause",
|
||||
KC.HOME : "home",
|
||||
KC.UP : "up",
|
||||
KC.PGUP : "page_up",
|
||||
KC.LEFT : "left",
|
||||
KC.RIGHT : "right",
|
||||
KC.END : "end",
|
||||
KC.DOWN : "down",
|
||||
KC.PGDOWN : "page_down",
|
||||
KC.INSERT : "insert",
|
||||
KC.DELETE : "delete",
|
||||
KC.LWIN : "left_win",
|
||||
KC.RWIN : "right_win",
|
||||
KC.APPS : "app_menu",
|
||||
KC.POWER : "power",
|
||||
KC.SLEEP : "sleep",
|
||||
KC.WAKE : "wake",
|
||||
KC.WEBSEARCH : "web_search",
|
||||
KC.WEBFAVORITES: "web_favorites",
|
||||
KC.WEBREFRESH : "web_refresh",
|
||||
KC.WEBSTOP : "web_stop",
|
||||
KC.WEBFORWARD : "web_forward",
|
||||
KC.WEBBACK : "web_back",
|
||||
KC.MYCOMPUTER : "my_computer",
|
||||
KC.MAIL : "mail",
|
||||
KC.MEDIASELECT : "media_select",
|
||||
|
||||
|
||||
KC.CharOnly : "CHAR_ONLY", // Set when the keysym is 0 but the
|
||||
// character is set. This happens
|
||||
// with many international
|
||||
// characters or reassigned
|
||||
// characters in OIS (and it
|
||||
// SUCKS.)
|
||||
|
||||
KC.Mouse0 : "mouse0",
|
||||
KC.Mouse1 : "mouse1",
|
||||
KC.Mouse2 : "mouse2",
|
||||
KC.Mouse3 : "mouse3",
|
||||
KC.Mouse4 : "mouse4",
|
||||
KC.Mouse5 : "mouse5",
|
||||
KC.Mouse6 : "mouse6",
|
||||
KC.Mouse7 : "mouse7",
|
||||
];
|
||||
|
||||
enum ComponentType : int
|
||||
{
|
||||
Unknown = 0,
|
||||
Button = 1, // ie. Key, mouse button, joy button, etc
|
||||
Axis = 2, // ie. A joystick or mouse axis
|
||||
Slider = 3, //
|
||||
POV = 4, // ie. Arrow direction keys
|
||||
Vector3 = 5 // ie. WiiMote orientation
|
||||
}
|
||||
|
||||
align(4) struct Axis
|
||||
{
|
||||
ComponentType type;
|
||||
int abs, rel;
|
||||
bool absOnly;
|
||||
}
|
||||
|
||||
// The C++ size of Axis is 16
|
||||
static assert(Axis.sizeof == 16);
|
||||
|
||||
struct MouseState
|
||||
{
|
||||
/* Represents the height/width of your display area.. used if mouse
|
||||
clipping or mouse grabbed in case of X11 - defaults to 50.. Make
|
||||
sure to set this and change when your size changes.. */
|
||||
int width, height;
|
||||
|
||||
// X Axis component
|
||||
Axis X;
|
||||
|
||||
// Y Axis Component
|
||||
Axis Y;
|
||||
|
||||
// Z Axis Component
|
||||
Axis Z;
|
||||
|
||||
// represents all buttons - bit position indicates button down
|
||||
int buttons;
|
||||
|
||||
// Button down test
|
||||
bool buttonDown( MB button )
|
||||
{
|
||||
return (buttons & ( 1 << button )) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we match the C++ size
|
||||
static assert(MouseState.sizeof == 60);
|
@ -1,41 +0,0 @@
|
||||
// Get current camera orientation, in the form of 'front' and 'up'
|
||||
// vectors.
|
||||
extern "C" void ogre_getCameraOrientation(float *fx, float *fy, float *fz,
|
||||
float *ux, float *uy, float *uz)
|
||||
{
|
||||
Vector3 front = mCamera->getDirection();
|
||||
Vector3 up = mCamera->getUp();
|
||||
*fx = front[0];
|
||||
*fy = -front[2];
|
||||
*fz = front[1];
|
||||
*ux = up[0];
|
||||
*uy = -up[2];
|
||||
*uz = up[1];
|
||||
}
|
||||
|
||||
// Move camera
|
||||
extern "C" void ogre_moveCamera(float x, float y, float z)
|
||||
{
|
||||
// Transforms Morrowind coordinates to OGRE coordinates. The camera
|
||||
// is not affected by the rotation of the root node, so we must
|
||||
// transform this manually.
|
||||
mCamera->setPosition(Vector3(x,z+90,-y));
|
||||
}
|
||||
|
||||
// Rotate camera using Morrowind rotation specifiers
|
||||
extern "C" void ogre_setCameraRotation(float r1, float r2, float r3)
|
||||
{
|
||||
// TODO: This translation is probably not correct, but for now I
|
||||
// have no reference point. Fix it later when we teleport from one
|
||||
// cell to another, so we have something to compare against.
|
||||
|
||||
// Rotate around X axis
|
||||
Quaternion xr(Radian(-r1), Vector3::UNIT_X);
|
||||
// Rotate around Y axis
|
||||
Quaternion yr(Radian(r3+3.14), Vector3::UNIT_Y);
|
||||
// Rotate around Z axis
|
||||
Quaternion zr(Radian(-r2), Vector3::UNIT_Z);
|
||||
|
||||
// Rotates first around z, then y, then x
|
||||
mCamera->setOrientation(xr*yr*zr);
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
// Copy a scene node and all its children
|
||||
void cloneNode(SceneNode *from, SceneNode *to, char* name)
|
||||
{
|
||||
to->setPosition(from->getPosition());
|
||||
to->setOrientation(from->getOrientation());
|
||||
to->setScale(from->getScale());
|
||||
|
||||
SceneNode::ObjectIterator it = from->getAttachedObjectIterator();
|
||||
while(it.hasMoreElements())
|
||||
{
|
||||
// We can't handle non-entities.
|
||||
Entity *e = dynamic_cast<Entity*> (it.getNext());
|
||||
if(e)
|
||||
{
|
||||
e = e->clone(String(name) + ":" + e->getName());
|
||||
to->attachObject(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively clone all child nodes
|
||||
SceneNode::ChildNodeIterator it2 = from->getChildIterator();
|
||||
while(it2.hasMoreElements())
|
||||
{
|
||||
cloneNode((SceneNode*)it2.getNext(), to->createChildSceneNode(), name);
|
||||
}
|
||||
}
|
||||
|
||||
// Supposed to insert a copy of the node, for now it just inserts the
|
||||
// actual node.
|
||||
extern "C" SceneNode *ogre_insertNode(SceneNode *base, char* name,
|
||||
float *pos, float *quat,
|
||||
float scale)
|
||||
{
|
||||
//std::cout << "ogre_insertNode(" << name << ")\n";
|
||||
SceneNode *node = mwRoot->createChildSceneNode(name);
|
||||
|
||||
// Make a copy of the node
|
||||
cloneNode(base, node, name);
|
||||
|
||||
// Apply transformations
|
||||
node->setPosition(pos[0], pos[1], pos[2]);
|
||||
node->setOrientation(quat[0], quat[1], quat[2], quat[3]);
|
||||
|
||||
node->setScale(scale, scale, scale);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// Get the world transformation of a node (the total transformation of
|
||||
// this node and all parent nodes). Return it as a translation
|
||||
// (3-vector) and a rotation / scaling part (3x3 matrix)
|
||||
extern "C" void ogre_getWorldTransform(SceneNode *node,
|
||||
float *trans, // Storage for translation
|
||||
float *matrix)// For 3x3 matrix
|
||||
{
|
||||
// Get the world transformation first
|
||||
Matrix4 trafo;
|
||||
node->getWorldTransforms(&trafo);
|
||||
|
||||
// Extract the translation part and pass it to the caller
|
||||
Vector3 tr = trafo.getTrans();
|
||||
trans[0] = tr[0];
|
||||
trans[1] = tr[1];
|
||||
trans[2] = tr[2];
|
||||
|
||||
// Next extract the matrix
|
||||
Matrix3 mat;
|
||||
trafo.extract3x3Matrix(mat);
|
||||
matrix[0] = mat[0][0];
|
||||
matrix[1] = mat[0][1];
|
||||
matrix[2] = mat[0][2];
|
||||
matrix[3] = mat[1][0];
|
||||
matrix[4] = mat[1][1];
|
||||
matrix[5] = mat[1][2];
|
||||
matrix[6] = mat[2][0];
|
||||
matrix[7] = mat[2][1];
|
||||
matrix[8] = mat[2][2];
|
||||
}
|
||||
|
||||
// Create the water plane. It doesn't really resemble "water" yet
|
||||
// though.
|
||||
extern "C" void ogre_createWater(float level)
|
||||
{
|
||||
// Create a plane aligned with the xy-plane.
|
||||
MeshManager::getSingleton().createPlane("water",
|
||||
ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
|
||||
Plane(Vector3::UNIT_Z, level),
|
||||
150000,150000
|
||||
);
|
||||
Entity *ent = mSceneMgr->createEntity( "WaterEntity", "water" );
|
||||
mwRoot->createChildSceneNode()->attachObject(ent);
|
||||
ent->setCastShadows(false);
|
||||
}
|
||||
|
||||
extern "C" SceneNode *ogre_getDetachedNode()
|
||||
{
|
||||
SceneNode *node = mwRoot->createChildSceneNode();
|
||||
mwRoot->removeChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
extern "C" SceneNode* ogre_createNode(
|
||||
char *name,
|
||||
float *trafo,
|
||||
SceneNode *parent,
|
||||
int32_t noRot)
|
||||
{
|
||||
//std::cout << "ogre_createNode(" << name << ")";
|
||||
SceneNode *node = parent->createChildSceneNode(name);
|
||||
//std::cout << " ... done\n";
|
||||
|
||||
// First is the translation vector
|
||||
|
||||
// TODO should be "if(!noRot)" only for exterior cells!? Yay for
|
||||
// consistency. Apparently, the displacement of the base node in NIF
|
||||
// files must be ignored for meshes in interior cells, but not for
|
||||
// exterior cells. Or at least that's my hypothesis, and it seems
|
||||
// work. There might be some other NIF trickery going on though, you
|
||||
// never know when you're reverse engineering someone else's file
|
||||
// format. We will handle this later.
|
||||
if(!noRot)
|
||||
node->setPosition(trafo[0], trafo[1], trafo[2]);
|
||||
|
||||
// Then a 3x3 rotation matrix.
|
||||
if(!noRot)
|
||||
node->setOrientation(Quaternion(Matrix3(trafo[3], trafo[4], trafo[5],
|
||||
trafo[6], trafo[7], trafo[8],
|
||||
trafo[9], trafo[10], trafo[11]
|
||||
)));
|
||||
|
||||
// Scale is at the end
|
||||
node->setScale(trafo[12],trafo[12],trafo[12]);
|
||||
|
||||
return node;
|
||||
}
|
@ -1,391 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (meshloader.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 ogre.meshloader;
|
||||
|
||||
import std.stdio;
|
||||
import std.stream;
|
||||
|
||||
import nif.nif;
|
||||
import nif.record;
|
||||
|
||||
import core.resource;
|
||||
import ogre.bindings;
|
||||
|
||||
import bullet.bindings;
|
||||
|
||||
import util.uniquename;
|
||||
|
||||
/*
|
||||
There are some problems that will have to be looked into later:
|
||||
|
||||
- Some meshes crash Ogre when shadows are turned on. (Not tested in
|
||||
newer versions of Ogre). Shadows are completely disabled for now.
|
||||
- There are obviously some boundry problems, some times the mesh
|
||||
disappears even though some part of it is inside the screen. This
|
||||
is especially a problem with animated meshes, since the animation
|
||||
might step outside the original bounding box.
|
||||
*/
|
||||
|
||||
MeshLoader meshLoader;
|
||||
|
||||
struct MeshLoader
|
||||
{
|
||||
// Not sure how to handle the bounding box, just ignore it for now.
|
||||
|
||||
char[] baseName; // NIF file name. Used in scene node names etc. so
|
||||
// that we can identify where they came from in
|
||||
// case of error messages.
|
||||
|
||||
// Load a NIF mesh. Assumes nifMesh is already opened. This creates
|
||||
// a "template" scene node containing this mesh, and removes it from
|
||||
// the main scene. This node can later be "cloned" so that multiple
|
||||
// instances of the object can be inserted into the world without
|
||||
// inserting the mesh more than once.
|
||||
void loadMesh(char[] name, out NodePtr base, out BulletShape shape)
|
||||
{
|
||||
baseName = name;
|
||||
|
||||
// Check if the first record is a node
|
||||
Node n = cast(Node) nifMesh.records[0];
|
||||
|
||||
if(n is null)
|
||||
{
|
||||
// TODO: Figure out what to do in this case, we should
|
||||
// probably throw.
|
||||
writefln("NIF '%s' IS NOT A MESH", name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get a fresh SceneNode and detatch it from the root. We use this
|
||||
// as the base for our mesh.
|
||||
base = ogre_getDetachedNode();
|
||||
|
||||
// Recursively insert nodes (don't rotate the first node)
|
||||
insertNode(n, base, 0, true);
|
||||
|
||||
// Get the final shape, if any
|
||||
shape = bullet_getFinalShape();
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
void insertNode(Node data, NodePtr parent,
|
||||
int flags,
|
||||
bool noRot = false)
|
||||
{
|
||||
// Add the flags to the previous node's flags
|
||||
flags = data.flags | flags;
|
||||
|
||||
// Create a scene node, move and rotate it into place. The name
|
||||
// must be unique, however we might have to recognize some special
|
||||
// names later, in order to attach arms and legs on NPCs
|
||||
// etc. Always ignore transformation of the first node? This is a
|
||||
// problem, I don't know when to do this and when not to. Neither
|
||||
// is always right. Update: I originally thought noRot should be
|
||||
// false for exteriors and true for interiors, but this isn't so.
|
||||
NodePtr node = ogre_createNode(UniqueName(data.name).ptr, &data.trafo,
|
||||
parent, cast(int)noRot);
|
||||
|
||||
// Handle any general properties here
|
||||
|
||||
// Call functions that do node-specific things, like handleNiNode
|
||||
// or handleNiTriShape.
|
||||
{
|
||||
NiNode n = cast(NiNode)data;
|
||||
if(n !is null)
|
||||
// Handle the NiNode, and any children it might have
|
||||
handleNiNode(n, node, flags);
|
||||
}
|
||||
|
||||
{
|
||||
NiTriShape n = cast(NiTriShape)data;
|
||||
if(n !is null)
|
||||
// Trishape, with a mesh
|
||||
handleNiTriShape(n, node, flags);
|
||||
}
|
||||
}
|
||||
|
||||
void handleNiNode(NiNode data, NodePtr node, int flags)
|
||||
{
|
||||
// Ignore sound activators and similar objects.
|
||||
NiStringExtraData d = cast(NiStringExtraData) data.extra;
|
||||
if(d !is null)
|
||||
{
|
||||
// Marker objects are only visible in the editor. We
|
||||
// completely ignore them.
|
||||
if(d.string == "MRK")
|
||||
return;
|
||||
|
||||
// No collision
|
||||
if(d.string == "NCO")
|
||||
flags |= 0x800; // Temporary internal marker
|
||||
}
|
||||
|
||||
// Handle any effects here
|
||||
|
||||
// In the cases where meshes have skeletal animations, we must
|
||||
// insert the children as bones in a skeleton instead, like we
|
||||
// originally did for all nodes. Update: A much better way is to
|
||||
// first insert the nodes normally, and then create the
|
||||
// skeleton. The nodes can then be moved one by one over to the
|
||||
// appropriate bones.
|
||||
|
||||
// Check the controller
|
||||
auto cont = data.controller;
|
||||
while(cont !is null)
|
||||
{
|
||||
auto kc = cast(NiKeyframeController)cont;
|
||||
auto pc = cast(NiPathController)cont;
|
||||
if(kc !is null)
|
||||
{
|
||||
/*
|
||||
writefln("Found keyframe controller");
|
||||
writefln(" Node name was: %s", data.name);
|
||||
assert(cont.target is data);
|
||||
|
||||
auto kcd = kc.data;
|
||||
writefln(" Types: %s %s %s",
|
||||
kcd.rotType, kcd.traType, kcd.scaleType);
|
||||
*/
|
||||
|
||||
/*
|
||||
Adding keyframes:
|
||||
|
||||
Skeleton -> Animation -> NodeAnimationTrack nt;
|
||||
|
||||
TransformKeyFrame * tf = nt->createNodeKeyFrame(time);
|
||||
tf->setTranslate(Vector3);
|
||||
tf->setScale(Vector3);
|
||||
tf->setRotation(Quaternion);
|
||||
|
||||
nt->applyToNode(node, time);
|
||||
evt
|
||||
Animation an;
|
||||
an->apply(skeleton, time);
|
||||
*/
|
||||
}
|
||||
else if(pc !is null)
|
||||
{
|
||||
//writefln("Found path controller");
|
||||
assert(cont.target is data);
|
||||
}
|
||||
//else writefln("Other controller (%s)", cont);
|
||||
cont = cont.next;
|
||||
}
|
||||
|
||||
// Loop through children
|
||||
foreach(Node n; data.children)
|
||||
insertNode(n, node, flags);
|
||||
}
|
||||
|
||||
void handleNiTriShape(NiTriShape shape, NodePtr node, int flags)
|
||||
{
|
||||
char[] texture;
|
||||
char[] material;
|
||||
char[] newName = UniqueName(baseName);
|
||||
NiMaterialProperty mp;
|
||||
|
||||
// Special alpha settings, if the NiAlphaProperty is present
|
||||
int alphaFlags = -1;
|
||||
ubyte alphaTest;
|
||||
|
||||
bool hidden = (flags & 0x01) != 0; // Not displayed
|
||||
bool collide = (flags & 0x02) != 0; // Use this mesh for collision
|
||||
bool bbcollide = (flags & 0x04) != 0; // Use bounding box for
|
||||
// collision
|
||||
// Always use mesh collision for now
|
||||
if(bbcollide) collide = true;
|
||||
bbcollide = false;
|
||||
|
||||
// Things marked "NCO" should not collide with anything.
|
||||
if(flags & 0x800)
|
||||
{ collide = false; bbcollide=false; }
|
||||
|
||||
// Skip the entire material phase for hidden nodes
|
||||
if(hidden) goto nomaterial;
|
||||
|
||||
// Scan the property list for textures
|
||||
foreach(Property p; shape.properties)
|
||||
{
|
||||
// NiTexturingProperty block
|
||||
{
|
||||
NiTexturingProperty t = cast(NiTexturingProperty) p;
|
||||
if(t !is null && t.textures[0].inUse)
|
||||
{
|
||||
// Ignore all other options for now
|
||||
NiSourceTexture st = t.textures[0].texture;
|
||||
if(st.external)
|
||||
{
|
||||
// Find the resource for this texture
|
||||
TextureIndex ti = resources.lookupTexture(st.filename);
|
||||
// Insert a manual loader into OGRE
|
||||
// ti.load();
|
||||
|
||||
// Get the resource name. We use getNewName to get
|
||||
// the real texture name, not the lookup
|
||||
// name. NewName has been converted to .dds if
|
||||
// necessary, to match the file name in the bsa
|
||||
// archives.
|
||||
texture = ti.getNewName();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Internal textures
|
||||
texture = "BLAH";
|
||||
writefln("Internal texture, cannot read this yet.");
|
||||
writefln("Final resource name: '%s'", texture);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// NiAlphaProperty
|
||||
{
|
||||
NiAlphaProperty a = cast(NiAlphaProperty) p;
|
||||
if(a !is null)
|
||||
{
|
||||
alphaFlags = a.flags;
|
||||
alphaTest = a.threshold;
|
||||
}
|
||||
}
|
||||
|
||||
// NiMaterialProperty block
|
||||
{
|
||||
NiMaterialProperty tmp = cast(NiMaterialProperty) p;
|
||||
if(tmp !is null)
|
||||
{
|
||||
if(mp !is null) writefln("WARNING: More than one material!");
|
||||
mp = tmp;
|
||||
material = newName;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get a pointer to the texture name
|
||||
char* texturePtr;
|
||||
if(texture.length) texturePtr = toStringz(texture);
|
||||
else texturePtr = null;
|
||||
|
||||
// Create the material
|
||||
if(material.length)
|
||||
// A material is present. Use it.
|
||||
ogre_createMaterial(material.ptr, mp.ambient.array.ptr,
|
||||
mp.diffuse.array.ptr,
|
||||
mp.specular.array.ptr, mp.emissive.array.ptr,
|
||||
mp.glossiness, mp.alpha, texturePtr,
|
||||
alphaFlags, alphaTest);
|
||||
else if(texturePtr)
|
||||
{
|
||||
// Texture, but no material. Make a default one.
|
||||
writefln("WARNING: Making default material for %s", texture);
|
||||
float[3] zero;
|
||||
float[3] one;
|
||||
zero[] = 0.0;
|
||||
one[] = 1.0;
|
||||
|
||||
ogre_createMaterial(newName.ptr, one.ptr, one.ptr, zero.ptr, zero.ptr,
|
||||
0.0, 1.0, texturePtr, alphaFlags, alphaTest);
|
||||
}
|
||||
|
||||
nomaterial:
|
||||
|
||||
with(shape.data)
|
||||
{
|
||||
//writefln("Number of vertices: ", vertices.length);
|
||||
|
||||
float *normalsPtr;
|
||||
float *colorsPtr;
|
||||
float *uvsPtr;
|
||||
short *facesPtr;
|
||||
|
||||
// Point pointers into the correct arrays, if they are present. If
|
||||
// not, the pointers retain their values of null.
|
||||
if(normals.length) normalsPtr = normals.ptr;
|
||||
if(colors.length) colorsPtr = colors.ptr;
|
||||
if(uvlist.length) uvsPtr = uvlist.ptr;
|
||||
if(triangles.length) facesPtr = triangles.ptr;
|
||||
|
||||
float
|
||||
minX = float.infinity,
|
||||
minY = float.infinity,
|
||||
minZ = float.infinity,
|
||||
maxX = -float.infinity,
|
||||
maxY = -float.infinity,
|
||||
maxZ = -float.infinity;
|
||||
|
||||
// Calculate the bounding box. TODO: This is really a
|
||||
// hack. IIRC the bounding box supplied by the NIF could not
|
||||
// be trusted, but I can't remember why :/
|
||||
for( int i; i < vertices.length; i+=3 )
|
||||
{
|
||||
if( vertices[i] < minX ) minX = vertices[i];
|
||||
if( vertices[i+1] < minY ) minY = vertices[i+1];
|
||||
if( vertices[i+2] < minZ) minZ = vertices[i+2];
|
||||
|
||||
if( vertices[i] > maxX) maxX = vertices[i];
|
||||
if( vertices[i+1] > maxY) maxY = vertices[i+1];
|
||||
if( vertices[i+2] > maxZ) maxZ = vertices[i+2];
|
||||
}
|
||||
|
||||
// Get the node world transformation, needed to set up
|
||||
// the collision shape properly.
|
||||
float[3] trans;
|
||||
float[9] matrix;
|
||||
ogre_getWorldTransform(node, trans.ptr, matrix.ptr);
|
||||
|
||||
// Next we must create the actual OGRE mesh and the collision
|
||||
// objects, based on the flags we have been given. TODO: I
|
||||
// guess the NIF bounding box is better than the one we have
|
||||
// calculated ourselves? This code definitely doesn't work,
|
||||
// but I haven't look into why yet.
|
||||
assert(!bbcollide);
|
||||
if(bbcollide)
|
||||
// Insert the bounding box into the collision system
|
||||
bullet_createBoxShape(minX, minY, minZ, maxX, maxY, maxZ,
|
||||
trans.ptr, matrix.ptr);
|
||||
|
||||
// Create a bullet collision shape from the trimesh. Pass
|
||||
// along the world transformation as well, since we must
|
||||
// transform the trimesh data manually.
|
||||
else if(collide)
|
||||
{
|
||||
assert(facesPtr !is null,
|
||||
"cannot create collision shape without a mesh");
|
||||
bullet_createTriShape(triangles.length, facesPtr,
|
||||
vertices.length, vertices.ptr,
|
||||
trans.ptr, matrix.ptr);
|
||||
}
|
||||
|
||||
// Create the ogre mesh, associate it with the node. Skip for
|
||||
// hidden nodes.
|
||||
if(!hidden)
|
||||
ogre_createMesh(newName.ptr, vertices.length, vertices.ptr,
|
||||
normalsPtr, colorsPtr, uvsPtr, triangles.length,
|
||||
facesPtr, radius, material.ptr, minX, minY, minZ,
|
||||
maxX, maxY, maxZ, node);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,471 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008-2009 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (openmw.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 openmw;
|
||||
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
import std.cstream;
|
||||
import std.file;
|
||||
|
||||
import ogre.ogre;
|
||||
import ogre.bindings;
|
||||
import gui.bindings;
|
||||
import gui.gui;
|
||||
|
||||
import bullet.bullet;
|
||||
|
||||
import scene.celldata;
|
||||
import scene.soundlist;
|
||||
import scene.gamesettings;
|
||||
import scene.player;
|
||||
|
||||
import core.resource;
|
||||
import core.memory;
|
||||
import core.config;
|
||||
|
||||
import monster.util.string;
|
||||
import monster.vm.mclass;
|
||||
import monster.vm.dbg;
|
||||
import mscripts.setup;
|
||||
|
||||
import sound.audio;
|
||||
|
||||
import input.events;
|
||||
|
||||
import terrain.terrain;
|
||||
|
||||
// Set up exit handler
|
||||
alias void function() c_func;
|
||||
extern(C) int atexit(c_func);
|
||||
|
||||
bool cleanExit = false;
|
||||
|
||||
void exitHandler()
|
||||
{
|
||||
// If we exit uncleanly, print the function stack.
|
||||
if(!cleanExit)
|
||||
writefln(dbg.getTrace());
|
||||
}
|
||||
|
||||
|
||||
//*
|
||||
import std.gc;
|
||||
import gcstats;
|
||||
|
||||
void poolSize()
|
||||
{
|
||||
GCStats gc;
|
||||
getStats(gc);
|
||||
writefln("Pool size: ", comma(gc.poolsize));
|
||||
writefln("Used size: ", comma(gc.usedsize));
|
||||
}
|
||||
//*/
|
||||
|
||||
void main(char[][] args)
|
||||
{
|
||||
bool render = true;
|
||||
bool help = false;
|
||||
bool resetKeys = false;
|
||||
bool showOgreFlag = false;
|
||||
bool debugOut = false;
|
||||
bool extTest = false;
|
||||
bool doGen = false;
|
||||
bool nextSave = false;
|
||||
bool loadSave = false;
|
||||
|
||||
// Some examples to try:
|
||||
//
|
||||
// "Abandoned Shipwreck, Upper Level";
|
||||
// "Gro-Bagrat Plantation";
|
||||
// "Abinabi";
|
||||
// "Abebaal Egg Mine";
|
||||
// "Ald-ruhn, Ald Skar Inn";
|
||||
// "Koal Cave";
|
||||
// "Ald-ruhn, Arobar Manor Bedrooms"
|
||||
// "Sud";
|
||||
// "Vivec, The Lizard's Head";
|
||||
// "ToddTest";
|
||||
|
||||
// Cells to load
|
||||
char[][] cells;
|
||||
|
||||
// Savegame to load
|
||||
char[] savefile;
|
||||
|
||||
foreach(char[] a; args[1..$])
|
||||
if(a == "-n") render = false;
|
||||
else if(a == "-ex") extTest = true;
|
||||
else if(a == "-gen") doGen = true;
|
||||
else if(a == "-h") help=true;
|
||||
else if(a == "-rk") resetKeys = true;
|
||||
else if(a == "-oc") showOgreFlag = true;
|
||||
else if(a == "-ns") config.noSound = true;
|
||||
else if(a == "-save") nextSave = true;
|
||||
else if(a == "-debug")
|
||||
{
|
||||
// Enable Monster debug output
|
||||
dbg.dbgOut = dout;
|
||||
|
||||
// Tell OGRE to do the same later on
|
||||
debugOut = true;
|
||||
}
|
||||
else if(nextSave)
|
||||
{
|
||||
savefile = a;
|
||||
nextSave = false;
|
||||
}
|
||||
else cells ~= a;
|
||||
|
||||
if(cells.length > 1)
|
||||
{
|
||||
writefln("More than one cell specified, rendering disabled");
|
||||
render=false;
|
||||
}
|
||||
|
||||
void showHelp()
|
||||
{
|
||||
writefln("Syntax: %s [options] cell-name [cell-name]", args[0]);
|
||||
writefln(" Options:");
|
||||
writefln(" -n Only load, do not render");
|
||||
writefln(" -ex Test the terrain system");
|
||||
writefln(" -gen Generate landscape cache");
|
||||
writefln(" -rk Reset key bindings to default");
|
||||
writefln(" -oc Show the Ogre config dialogue");
|
||||
writefln(" -ns Completely disable sound");
|
||||
writefln(" -debug Print debug information");
|
||||
writefln(" -save <file> Load cell/pos from savegame");
|
||||
writefln(" -h Show this help");
|
||||
writefln("");
|
||||
writefln("Specifying more than one cell implies -n");
|
||||
}
|
||||
|
||||
if(help)
|
||||
{
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
initializeMemoryRegions();
|
||||
initMonsterScripts();
|
||||
|
||||
// This is getting increasingly hackish, but this entire engine
|
||||
// design is now quickly outgrowing its usefulness, and a rewrite is
|
||||
// coming soon anyway.
|
||||
PlayerSaveInfo pi;
|
||||
if(savefile != "")
|
||||
{
|
||||
if(cells.length)
|
||||
{
|
||||
writefln("Please don't specify both a savegame file (%s) and cell names (%s)", savefile, cells);
|
||||
return;
|
||||
}
|
||||
|
||||
loadSave = true;
|
||||
writefln("Loading savegame %s", savefile);
|
||||
pi = importSavegame(savefile);
|
||||
writefln(" Player name: %s", pi.playerName);
|
||||
writefln(" Cell name: %s", pi.cellName);
|
||||
writefln(" Pos: %s", pi.pos.position);
|
||||
writefln(" Rot: %s", pi.pos.rotation);
|
||||
|
||||
cells = [pi.cellName];
|
||||
}
|
||||
|
||||
config.initialize(resetKeys);
|
||||
scope(exit) config.writeConfig();
|
||||
|
||||
// Check if the data directory exists
|
||||
if(!exists(config.dataDir) || !isdir(config.dataDir))
|
||||
{
|
||||
writefln("Cannot find data directory '", config.dataDir,
|
||||
"' - please edit openmw.ini.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the -oc parameter is specified, we override any config
|
||||
// setting.
|
||||
if(showOgreFlag) config.finalOgreConfig = true;
|
||||
|
||||
if(cells.length == 0)
|
||||
if(config.defaultCell.length)
|
||||
cells ~= config.defaultCell;
|
||||
|
||||
if(cells.length == 1 && !loadSave)
|
||||
config.defaultCell = cells[0];
|
||||
|
||||
if(cells.length == 0)
|
||||
{
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!config.noSound) initializeSound();
|
||||
resources.initResources();
|
||||
|
||||
// Load all ESM and ESP files
|
||||
loadTESFiles(config.gameFiles);
|
||||
|
||||
scene.gamesettings.loadGameSettings();
|
||||
|
||||
CellData cd = cellList.get();
|
||||
|
||||
foreach(char[] cName; cells)
|
||||
{
|
||||
// Release the last cell data
|
||||
cellList.release(cd);
|
||||
|
||||
// Get a cell data holder and load an interior cell
|
||||
cd = cellList.get();
|
||||
|
||||
try cd.loadIntCell(cName);
|
||||
catch(Exception e)
|
||||
{
|
||||
writefln("\nUnable to load cell '%s'.", cName);
|
||||
writefln("\nDetails: %s", e);
|
||||
writefln("
|
||||
Perhaps this cell does not exist in your Morrowind language version?
|
||||
Try specifying another cell name on the command line, or edit openmw.ini.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're loading from save, override the player position
|
||||
if(loadSave)
|
||||
*playerData.position = pi.pos;
|
||||
}
|
||||
|
||||
// Simple safety hack
|
||||
NodePtr putObject(MeshIndex m, Placement *pos, float scale,
|
||||
bool collide=false)
|
||||
{
|
||||
if(m is null)
|
||||
writefln("WARNING: CANNOT PUT NULL OBJECT");
|
||||
else if(!m.isEmpty)
|
||||
return placeObject(m, pos, scale, collide);
|
||||
|
||||
//writefln("WARNING: CANNOT INSERT EMPTY MESH '%s'", m.getName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(cd.inCell !is null)
|
||||
// Set the name for the GUI (temporary hack)
|
||||
gui_setCellName(cd.inCell.id.ptr);
|
||||
|
||||
// Set up the exit handler
|
||||
atexit(&exitHandler);
|
||||
scope(exit) cleanExit = true;
|
||||
|
||||
if(render)
|
||||
{
|
||||
// Warm up OGRE
|
||||
setupOgre(debugOut);
|
||||
scope(exit) cleanupOgre();
|
||||
|
||||
// Create the GUI system
|
||||
initGUI(debugOut);
|
||||
|
||||
// Set up Bullet
|
||||
initBullet();
|
||||
scope(exit) cleanupBullet();
|
||||
|
||||
// Initialize the internal input and event manager. The
|
||||
// lower-level input system (OIS) is initialized by the
|
||||
// setupOgre() call further up.
|
||||
initializeInput();
|
||||
|
||||
// Play some old tunes
|
||||
if(extTest)
|
||||
{
|
||||
// Exterior cell
|
||||
/*
|
||||
Color c;
|
||||
c.red = 180;
|
||||
c.green = 180;
|
||||
c.blue = 180;
|
||||
setAmbient(c, c, c, 0);
|
||||
|
||||
// Put in the water
|
||||
ogre_createWater(cd.water);
|
||||
|
||||
// Create an ugly sky
|
||||
ogre_makeSky();
|
||||
*/
|
||||
|
||||
initTerrain(doGen);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Interior cell
|
||||
assert(cd.inCell !is null);
|
||||
setAmbient(cd.ambi.ambient, cd.ambi.sunlight,
|
||||
cd.ambi.fog, cd.ambi.fogDensity);
|
||||
|
||||
// Not all interior cells have water
|
||||
if(cd.inCell.flags & CellFlags.HasWater)
|
||||
ogre_createWater(cd.water);
|
||||
|
||||
// Insert the meshes of statics into the scene
|
||||
foreach(ref LiveStatic ls; cd.statics)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale(), true);
|
||||
// Inventory lights
|
||||
foreach(ref LiveLight ls; cd.lights)
|
||||
{
|
||||
NodePtr n = putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
ls.lightNode = attachLight(n, ls.m.data.color, ls.m.data.radius);
|
||||
if(!config.noSound)
|
||||
{
|
||||
Sound *s = ls.m.sound;
|
||||
if(s)
|
||||
{
|
||||
ls.loopSound = soundScene.insert(s, true);
|
||||
if(ls.loopSound)
|
||||
{
|
||||
auto p = ls.getPos();
|
||||
ls.loopSound.setPos(p.position[0],
|
||||
p.position[1],
|
||||
p.position[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Static lights
|
||||
foreach(ref LiveLight ls; cd.statLights)
|
||||
{
|
||||
NodePtr n = putObject(ls.m.model, ls.getPos(), ls.getScale(), true);
|
||||
ls.lightNode = attachLight(n, ls.m.data.color, ls.m.data.radius);
|
||||
if(!config.noSound)
|
||||
{
|
||||
Sound *s = ls.m.sound;
|
||||
if(s)
|
||||
{
|
||||
ls.loopSound = soundScene.insert(s, true);
|
||||
if(ls.loopSound)
|
||||
{
|
||||
auto p = ls.getPos();
|
||||
ls.loopSound.setPos(p.position[0],
|
||||
p.position[1],
|
||||
p.position[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Misc items
|
||||
foreach(ref LiveMisc ls; cd.miscItems)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
/*
|
||||
// NPCs (these are complicated, usually do not have normal meshes)
|
||||
foreach(ref LiveNPC ls; cd.npcs)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
*/
|
||||
// Containers
|
||||
foreach(ref LiveContainer ls; cd.containers)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale(), true);
|
||||
// Doors
|
||||
foreach(ref LiveDoor ls; cd.doors)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Activators (including beds etc)
|
||||
foreach(ref LiveActivator ls; cd.activators)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale(), true);
|
||||
// Potions
|
||||
foreach(ref LivePotion ls; cd.potions)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Apparatus
|
||||
foreach(ref LiveApparatus ls; cd.appas)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Ingredients
|
||||
foreach(ref LiveIngredient ls; cd.ingredients)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Armors
|
||||
foreach(ref LiveArmor ls; cd.armors)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Weapons
|
||||
foreach(ref LiveWeapon ls; cd.weapons)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Books
|
||||
foreach(ref LiveBook ls; cd.books)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Clothes
|
||||
foreach(ref LiveClothing ls; cd.clothes)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Tools
|
||||
foreach(ref LiveTool ls; cd.tools)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
// Creatures (not displayed very well yet)
|
||||
foreach(ref LiveCreature ls; cd.creatures)
|
||||
putObject(ls.m.model, ls.getPos(), ls.getScale());
|
||||
|
||||
// End of interior cell
|
||||
}
|
||||
|
||||
// Run GUI system
|
||||
startGUI();
|
||||
|
||||
// Play some old tunes
|
||||
if(!config.noSound)
|
||||
Music.play();
|
||||
|
||||
// Run it until the user tells us to quit
|
||||
startRendering();
|
||||
}
|
||||
else if(debugOut) writefln("Skipping rendering");
|
||||
|
||||
if(!config.noSound)
|
||||
{
|
||||
soundScene.kill();
|
||||
shutdownSound();
|
||||
}
|
||||
|
||||
if(debugOut)
|
||||
{
|
||||
writefln();
|
||||
writefln("%d statics", cd.statics.length);
|
||||
writefln("%d misc items", cd.miscItems.length);
|
||||
writefln("%d inventory lights", cd.lights.length);
|
||||
writefln("%d static lights", cd.statLights.length);
|
||||
writefln("%d NPCs", cd.npcs.length);
|
||||
writefln("%d containers", cd.containers.length);
|
||||
writefln("%d doors", cd.doors.length);
|
||||
writefln("%d activators", cd.activators.length);
|
||||
writefln("%d potions", cd.potions.length);
|
||||
writefln("%d apparatuses", cd.appas.length);
|
||||
writefln("%d ingredients", cd.ingredients.length);
|
||||
writefln("%d armors", cd.armors.length);
|
||||
writefln("%d weapons", cd.weapons.length);
|
||||
writefln("%d books", cd.books.length);
|
||||
writefln("%d tools", cd.tools.length);
|
||||
writefln("%d clothes", cd.clothes.length);
|
||||
writefln("%d creatures", cd.creatures.length);
|
||||
writefln();
|
||||
}
|
||||
|
||||
// This isn't necessary but it's here for testing purposes.
|
||||
cellList.release(cd);
|
||||
|
||||
// Write some statistics
|
||||
if(debugOut)
|
||||
{
|
||||
poolSize();
|
||||
writefln(esmRegion);
|
||||
writefln("Total objects: ", MonsterClass.getTotalObjects);
|
||||
}
|
||||
}
|
@ -1,461 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2009 Nicolay Korslund
|
||||
WWW: http://openmw.sourceforge.net/
|
||||
|
||||
This file (archive.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 terrain.archive;
|
||||
|
||||
const float TEX_SCALE = 1.0/16;
|
||||
|
||||
// This should be part of the generic cache system.
|
||||
const int CACHE_MAGIC = 0x345AF815;
|
||||
|
||||
import std.mmfile;
|
||||
import std.string;
|
||||
import std.stdio;
|
||||
import terrain.myfile;
|
||||
|
||||
version(Windows)
|
||||
static int pageSize = 64*1024;
|
||||
else
|
||||
static int pageSize = 4*1024;
|
||||
|
||||
extern(C)
|
||||
{
|
||||
// Convert a texture index to string
|
||||
char *d_terr_getTexName(int index)
|
||||
{ return g_archive.getString(index).ptr; }
|
||||
|
||||
// Fill various hardware buffers from cache
|
||||
void d_terr_fillVertexBuffer(MeshInfo *mi, float *buffer, ulong size)
|
||||
{ mi.fillVertexBuffer(buffer[0..size]); }
|
||||
|
||||
void d_terr_fillIndexBuffer(MeshInfo *mi, ushort *buffer, ulong size)
|
||||
{ mi.fillIndexBuffer(buffer[0..size]); }
|
||||
|
||||
void d_terr_fillAlphaBuffer(AlphaInfo *mi, ubyte *buffer, ulong size)
|
||||
{ mi.fillAlphaBuffer(buffer[0..size]); }
|
||||
|
||||
// Get a given alpha map struct belonging to a mesh
|
||||
AlphaInfo *d_terr_getAlphaInfo(MeshInfo *mi, int index)
|
||||
{ return mi.getAlphaInfo(index); }
|
||||
|
||||
int d_terr_getAlphaSize() { return g_archive.alphaSize; }
|
||||
}
|
||||
|
||||
// Info about the entire quad. TODO: Some of this (such as the texture
|
||||
// scale and probably the width and radius) can be generated at
|
||||
// loadtime and is common for all quads on the same level. We could
|
||||
// just make a QuadLevelInfo struct.
|
||||
struct QuadInfo
|
||||
{
|
||||
// Basic info
|
||||
int cellX, cellY;
|
||||
int level;
|
||||
|
||||
// Bounding box info
|
||||
float minHeight, maxHeight;
|
||||
float worldWidth;
|
||||
float boundingRadius;
|
||||
|
||||
// True if we should make the given child
|
||||
bool hasChild[4];
|
||||
|
||||
// Number of mesh segments in this quad
|
||||
int meshNum;
|
||||
|
||||
// Location of this quad in the main archive file. The size includes
|
||||
// everything related to this quad, including mesh data, alpha maps,
|
||||
// etc.
|
||||
size_t offset, size;
|
||||
}
|
||||
|
||||
// Info about an alpha map belonging to a mesh
|
||||
struct AlphaInfo
|
||||
{
|
||||
// Position of the actual image data
|
||||
ulong bufSize, bufOffset;
|
||||
|
||||
// The texture name for this layer. The actual string is stored in
|
||||
// the archive's string buffer.
|
||||
int texName = -1;
|
||||
int alphaName = -1;
|
||||
|
||||
// Fill the alpha texture buffer
|
||||
void fillAlphaBuffer(ubyte abuf[])
|
||||
{
|
||||
assert(abuf.length == bufSize);
|
||||
g_archive.copy(abuf.ptr, bufOffset, bufSize);
|
||||
}
|
||||
}
|
||||
static assert(AlphaInfo.sizeof == 6*4);
|
||||
|
||||
// Info about each submesh
|
||||
// If you change this struct please check whether align(1) still fits.
|
||||
align(1)
|
||||
struct MeshInfo
|
||||
{
|
||||
// Bounding box info
|
||||
float minHeight, maxHeight;
|
||||
float worldWidth;
|
||||
|
||||
// Vertex and index numbers
|
||||
int vertRows, vertCols;
|
||||
|
||||
// Height offset to apply to all vertices
|
||||
float heightOffset;
|
||||
|
||||
// Size and offset of the vertex buffer
|
||||
ulong vertBufSize, vertBufOffset;
|
||||
|
||||
// Texture name. Index to the string table.
|
||||
int texName = -1;
|
||||
|
||||
// Number and offset of AlphaInfo blocks
|
||||
int alphaNum;
|
||||
ulong alphaOffset;
|
||||
|
||||
// Fill the given vertex buffer
|
||||
void fillVertexBuffer(float vdest[])
|
||||
{
|
||||
// The height map and normals from the archive
|
||||
byte *hmap = cast(byte*)g_archive.getRelSlice(vertBufOffset, vertBufSize).ptr;
|
||||
// The generic part, containing the x,y coordinates and the uv
|
||||
// maps.
|
||||
float *gmap = g_archive.getVertexBuffer(getLevel()).ptr;
|
||||
|
||||
// Destination pointer
|
||||
float *vbuf = vdest.ptr;
|
||||
assert(vdest.length == vertRows*vertCols*8);
|
||||
|
||||
// Merge the two data sets together into the output buffer.
|
||||
float offset = heightOffset;
|
||||
for(int y=0; y<vertRows; y++)
|
||||
{
|
||||
// The offset for the entire row is determined by the first
|
||||
// height value. All the values in a row gives the height
|
||||
// relative to the previous value, and the first value in each
|
||||
// row is relative to the first value in the previous row.
|
||||
offset += *cast(short*)hmap;
|
||||
|
||||
// This is the 'sliding offset' for this row. It's adjusted
|
||||
// for each vertex that's added, but only affects this row.
|
||||
float rowofs = offset;
|
||||
for(int x=0; x<vertCols; x++)
|
||||
{
|
||||
hmap+=2; // Skip the height we just read
|
||||
|
||||
// X and Y from the pregenerated buffer
|
||||
*vbuf++ = *gmap++;
|
||||
*vbuf++ = *gmap++;
|
||||
|
||||
// The height is calculated from the current offset
|
||||
*vbuf++ = rowofs * 8;
|
||||
|
||||
// Normal vector.
|
||||
*vbuf++ = *hmap++;
|
||||
*vbuf++ = *hmap++;
|
||||
*vbuf++ = *hmap++;
|
||||
|
||||
// UV
|
||||
*vbuf++ = *gmap++;
|
||||
*vbuf++ = *gmap++;
|
||||
|
||||
// Adjust the offset for the next vertex.
|
||||
if(x < vertCols-1)
|
||||
rowofs += *cast(short*)hmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the index buffer
|
||||
void fillIndexBuffer(ushort ibuf[])
|
||||
{
|
||||
// The index buffer is pregenerated. It is identical for all
|
||||
// meshes on the same level, so just copy it over.
|
||||
ushort generic[] = g_archive.getIndexBuffer();
|
||||
assert(ibuf.length == generic.length);
|
||||
ibuf[] = generic[];
|
||||
}
|
||||
|
||||
int getLevel()
|
||||
{
|
||||
assert(g_archive.curQuad);
|
||||
return g_archive.curQuad.level;
|
||||
}
|
||||
|
||||
// Get an alpha map belonging to this mesh
|
||||
AlphaInfo *getAlphaInfo(int num)
|
||||
{
|
||||
assert(num < alphaNum && num >= 0);
|
||||
assert(getLevel() == 1);
|
||||
AlphaInfo *res = cast(AlphaInfo*)g_archive.getRelSlice
|
||||
(alphaOffset, alphaNum*AlphaInfo.sizeof);
|
||||
res += num;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
static assert(MeshInfo.sizeof == 14*4);
|
||||
|
||||
// The first part of the .index file
|
||||
struct ArchiveHeader
|
||||
{
|
||||
// "Magic" number to make sure we're actually reading an archive
|
||||
// file
|
||||
int magic;
|
||||
|
||||
// Total number of quads in the archive
|
||||
int quads;
|
||||
|
||||
// Level of the 'root' quad. There will only be one quad on this
|
||||
// level.
|
||||
int rootLevel;
|
||||
|
||||
// Size of the alpha maps, in pixels along one side.
|
||||
int alphaSize;
|
||||
|
||||
// Number of strings in the string table
|
||||
int stringNum;
|
||||
|
||||
// Size of the string buffer
|
||||
size_t stringSize;
|
||||
}
|
||||
|
||||
TerrainArchive g_archive;
|
||||
|
||||
// This class handles the cached terrain data.
|
||||
struct TerrainArchive
|
||||
{
|
||||
MeshInfo *curMesh;
|
||||
QuadInfo *curQuad;
|
||||
QuadInfo *rootQuad;
|
||||
|
||||
void openFile(char[] name)
|
||||
{
|
||||
mmf = new MmFile(name,
|
||||
MmFile.Mode.Read,
|
||||
0, null, pageSize);
|
||||
|
||||
// Read the index file first
|
||||
MyFile ifile = new MyFile(name ~ ".index");
|
||||
|
||||
ArchiveHeader head;
|
||||
ifile.fill(head);
|
||||
|
||||
// Reads data into an array. Would be better if this was part of
|
||||
// the stream.
|
||||
|
||||
// Sanity check
|
||||
assert(head.magic == CACHE_MAGIC);
|
||||
assert(head.quads > 0 && head.quads < 8192);
|
||||
|
||||
// Store header info
|
||||
alphaSize = head.alphaSize;
|
||||
|
||||
// Read all the quads
|
||||
quadList = new QuadInfo[head.quads];
|
||||
ifile.fillArray(quadList);
|
||||
|
||||
// Create an index of all the quads
|
||||
foreach(int index, qn; quadList)
|
||||
{
|
||||
int x = qn.cellX;
|
||||
int y = qn.cellY;
|
||||
int l = qn.level;
|
||||
|
||||
assert(l >= 1);
|
||||
|
||||
quadMap[l][x][y] = index;
|
||||
assert(index == quadMap[l][x][y]);
|
||||
|
||||
// Store the root quad
|
||||
if(l == head.rootLevel)
|
||||
{
|
||||
assert(rootQuad == null);
|
||||
rootQuad = &quadList[index];
|
||||
}
|
||||
else
|
||||
assert(l < head.rootLevel);
|
||||
}
|
||||
|
||||
// Make sure the root was set
|
||||
assert(rootQuad !is null);
|
||||
|
||||
// Next read the string table. First read the main string buffer.
|
||||
stringBuf = new char[head.stringSize];
|
||||
ifile.fillArray(stringBuf);
|
||||
|
||||
// Then read the string offsets
|
||||
int[] offsets = new int[head.stringNum];
|
||||
ifile.fillArray(offsets);
|
||||
|
||||
// Set up the string table
|
||||
char *strptr = stringBuf.ptr;
|
||||
strings.length = head.stringNum;
|
||||
foreach(int i, ref str; strings)
|
||||
{
|
||||
// toString(char*) returns the string up to the zero
|
||||
// terminator byte
|
||||
str = toString(strptr + offsets[i]);
|
||||
assert(str.ptr + str.length <=
|
||||
stringBuf.ptr + stringBuf.length);
|
||||
}
|
||||
delete offsets;
|
||||
|
||||
// Read the vertex buffer data
|
||||
int bufNum = head.rootLevel;
|
||||
assert(bufNum == 7);
|
||||
vertBufData.length = bufNum;
|
||||
|
||||
// Fill the vertex buffers. Start at level 1.
|
||||
for(int i=1;i<bufNum;i++)
|
||||
{
|
||||
// Vertex buffer
|
||||
ifile.readArray(vertBufData[i]);
|
||||
}
|
||||
|
||||
// Index buffer
|
||||
ifile.readArray(indexBufData);
|
||||
}
|
||||
|
||||
bool hasQuad(int X, int Y, int level)
|
||||
{
|
||||
if((level in quadMap) is null ||
|
||||
(X in quadMap[level]) is null ||
|
||||
(Y in quadMap[level][X]) is null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get info about a given quad from the index.
|
||||
QuadInfo *getQuad(int X, int Y, int level)
|
||||
{
|
||||
assert(hasQuad(X,Y,level), format("Cannot find quad %s %s level %s",
|
||||
X, Y, level));
|
||||
int ind = quadMap[level][X][Y];
|
||||
QuadInfo *res = &quadList[ind];
|
||||
assert(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Maps the terrain and material info for a given quad into
|
||||
// memory. This is typically called right before the meshes are
|
||||
// created.
|
||||
void mapQuad(QuadInfo *info)
|
||||
{
|
||||
assert(info);
|
||||
|
||||
// Store the quad for later
|
||||
curQuad = info;
|
||||
|
||||
doMap(info.offset, info.size);
|
||||
}
|
||||
|
||||
// Get the info struct for a given segment. Remembers the MeshInfo
|
||||
// for all later calls.
|
||||
MeshInfo *getMeshInfo(int segNum)
|
||||
{
|
||||
assert(curQuad);
|
||||
assert(segNum < curQuad.meshNum);
|
||||
|
||||
// The mesh headers are at the beginning of the mapped segment.
|
||||
curMesh = cast(MeshInfo*) getRelSlice(0, MeshInfo.sizeof*curQuad.meshNum);
|
||||
curMesh += segNum;
|
||||
|
||||
return curMesh;
|
||||
}
|
||||
|
||||
float[] getVertexBuffer(int level)
|
||||
{
|
||||
assert(level>=1 && level<vertBufData.length);
|
||||
return vertBufData[level];
|
||||
}
|
||||
|
||||
ushort[] getIndexBuffer()
|
||||
{
|
||||
return indexBufData;
|
||||
}
|
||||
|
||||
private:
|
||||
// All quad headers (from the index) are stored in this array
|
||||
QuadInfo quadList[];
|
||||
|
||||
// A map of all quads. Contain indices to the above array. Indexed
|
||||
// by [level][X][Y].
|
||||
int[int][int][int] quadMap;
|
||||
|
||||
// These contain pregenerated mesh data that is common for all
|
||||
// meshes on a given level.
|
||||
float[][] vertBufData;
|
||||
ushort[] indexBufData;
|
||||
|
||||
// Used for the mmapped file
|
||||
MmFile mmf;
|
||||
|
||||
ubyte mapped[];
|
||||
|
||||
// Stores the string table
|
||||
char[] stringBuf;
|
||||
char[][] strings;
|
||||
|
||||
// Texture size of the alpha maps.
|
||||
int alphaSize;
|
||||
|
||||
char[] getString(int index)
|
||||
{
|
||||
assert(index >= 0);
|
||||
assert(index < strings.length);
|
||||
|
||||
return strings[index];
|
||||
}
|
||||
|
||||
void doMap(size_t offset, size_t size)
|
||||
{
|
||||
assert(mmf !is null);
|
||||
assert(size);
|
||||
mapped = cast(ubyte[])mmf[offset..offset+size];
|
||||
assert(mapped.length == size);
|
||||
}
|
||||
|
||||
// Get a slice of a given buffer within the mapped window. The
|
||||
// offset is relative to the start of the window, and the size must
|
||||
// fit inside the window.
|
||||
ubyte[] getRelSlice(size_t offset, size_t size)
|
||||
{
|
||||
assert(mapped.length);
|
||||
|
||||
return mapped[offset..offset+size];
|
||||
}
|
||||
|
||||
// Copy a given buffer from the file. The buffer might be a
|
||||
// compressed stream, so it's important that the buffers are written
|
||||
// in the same block sizes as they are read. (Ie. you can't write a
|
||||
// buffer as one operation and read it as two, or vice versa. Also,
|
||||
// buffers cannot overlap.) The offset is relative to the current
|
||||
// mapped file window.
|
||||
void copy(void *dst, size_t offset, size_t inSize)
|
||||
{
|
||||
ubyte source[] = getRelSlice(offset, inSize);
|
||||
|
||||
// Just copy it for now
|
||||
ubyte* dest = cast(ubyte*)dst;
|
||||
dest[0..source.length] = source[];
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
module terrain.bindings;
|
||||
|
||||
alias void *SceneNode;
|
||||
alias void *Bounds;
|
||||
alias void *MeshObj;
|
||||
|
||||
// These are all defined in cpp_terrain.cpp:
|
||||
extern(C):
|
||||
|
||||
SceneNode terr_createChildNode(float relX, float relY, SceneNode);
|
||||
void terr_destroyNode(SceneNode);
|
||||
Bounds terr_makeBounds(float minHeight, float maxHeight, float width, SceneNode);
|
||||
void terr_killBounds(Bounds);
|
||||
float terr_getSqCamDist(Bounds);
|
||||
MeshObj terr_makeMesh(SceneNode,void*,int,float);
|
||||
void terr_killMesh(MeshObj);
|
||||
|
||||
void terr_genData();
|
||||
void terr_setupRendering();
|
||||
|
||||
void terr_makeLandMaterial(char*,float);
|
||||
ubyte *terr_makeAlphaLayer(char*,int);
|
||||
void terr_closeAlpha(char*,char*,float);
|
||||
void terr_cleanupAlpha(char*,void*,int);
|
||||
|
||||
void terr_resize(void*,void*,int,int);
|
||||
void terr_saveImage(void*,int,char*);
|
@ -1,337 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2009 Nicolay Korslund
|
||||
WWW: http://openmw.sourceforge.net/
|
||||
|
||||
This file (cachewriter.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 terrain.cachewriter;
|
||||
|
||||
import terrain.archive;
|
||||
|
||||
import terrain.outbuffer;
|
||||
import std.stdio, std.stream, std.string;
|
||||
import terrain.myfile;
|
||||
import std.math2;
|
||||
import monster.util.string;
|
||||
import monster.vm.dbg;
|
||||
|
||||
// Helper structs
|
||||
struct AlphaHolder
|
||||
{
|
||||
AlphaInfo info;
|
||||
|
||||
// Actual pixel buffer
|
||||
ubyte[] buffer;
|
||||
}
|
||||
|
||||
struct MeshHolder
|
||||
{
|
||||
MeshInfo info;
|
||||
|
||||
// Actual buffers
|
||||
byte[] vertexBuffer;
|
||||
|
||||
// Alpha maps (if any)
|
||||
AlphaHolder alphas[];
|
||||
}
|
||||
|
||||
// A struct that gathers all the relevant quad data in one place.
|
||||
struct QuadHolder
|
||||
{
|
||||
QuadInfo info;
|
||||
|
||||
MeshHolder meshes[];
|
||||
}
|
||||
|
||||
struct CacheWriter
|
||||
{
|
||||
// Opens the main archive file for output
|
||||
void openFile(char[] fname)
|
||||
{
|
||||
mainFile = new File(fname, FileMode.OutNew);
|
||||
iname = fname ~ ".index";
|
||||
|
||||
buf = new OutBuffer;
|
||||
}
|
||||
|
||||
void setParams(int mxLev, int alphSize)
|
||||
{
|
||||
maxLevel = mxLev;
|
||||
alphaSize = alphSize;
|
||||
|
||||
vertBuf.length = maxLevel;
|
||||
}
|
||||
|
||||
// Closes the main archive file and writes the index.
|
||||
void finish()
|
||||
{
|
||||
mainFile.close();
|
||||
|
||||
// Write the index file
|
||||
scope MyFile ofile = new MyFile(iname, FileMode.OutNew);
|
||||
|
||||
// Header first
|
||||
ArchiveHeader head;
|
||||
head.magic = CACHE_MAGIC;
|
||||
head.quads = quadList.length;
|
||||
head.rootLevel = maxLevel;
|
||||
head.alphaSize = alphaSize;
|
||||
head.stringNum = stringList.length;
|
||||
head.stringSize = totalStringLength;
|
||||
ofile.dump(head);
|
||||
|
||||
// Write the quads
|
||||
ofile.dumpArray(quadList);
|
||||
|
||||
// String table next. We need to sort it in order of the indices
|
||||
// first.
|
||||
char[][] strVector;
|
||||
strVector.length = head.stringNum;
|
||||
|
||||
foreach(char[] key, int value; stringList)
|
||||
strVector[value] = key;
|
||||
|
||||
// Next, write the strings to file while we fill in the offset
|
||||
// list
|
||||
int[] offsets = new int[head.stringNum];
|
||||
|
||||
size_t curOffs = 0;
|
||||
for(int i=0; i<head.stringNum; i++)
|
||||
{
|
||||
// Add one byte for the zero terminator
|
||||
int len = strVector[i].length + 1;
|
||||
char *ptr = strVector[i].ptr;
|
||||
|
||||
if(ptr[len-1] != 0)
|
||||
ptr = toStringz(strVector[i]);
|
||||
|
||||
assert(ptr[len-1] == 0);
|
||||
|
||||
ofile.writeExact(ptr, len);
|
||||
|
||||
// Store the offset
|
||||
offsets[i] = curOffs;
|
||||
curOffs += len;
|
||||
}
|
||||
// At the end the offset should match the buffer size we set in
|
||||
// the header.
|
||||
assert(curOffs == head.stringSize);
|
||||
|
||||
// Finally, write the offset table itself
|
||||
ofile.dumpArray(offsets);
|
||||
|
||||
// Write the common vertex and index buffers
|
||||
assert(maxLevel == 7);
|
||||
for(int i=1;i<maxLevel;i++)
|
||||
{
|
||||
// Write vertex buffer
|
||||
ofile.writeArray(vertBuf[i]);
|
||||
delete vertBuf[i];
|
||||
}
|
||||
|
||||
// Then the index buffer
|
||||
ofile.writeArray(indexBuf);
|
||||
|
||||
// Don't need these anymore
|
||||
delete offsets;
|
||||
delete strVector;
|
||||
delete quadList;
|
||||
delete vertBuf;
|
||||
delete indexBuf;
|
||||
delete buf;
|
||||
delete mainFile;
|
||||
}
|
||||
|
||||
// Add a common vertex buffer for a given level
|
||||
void addVertexBuffer(int level, float[] buf)
|
||||
{
|
||||
assert(vertBuf.length > level);
|
||||
vertBuf[level] = buf;
|
||||
}
|
||||
|
||||
// Add a common index buffer
|
||||
void setIndexBuffer(ushort[] buf)
|
||||
{
|
||||
indexBuf = buf;
|
||||
}
|
||||
|
||||
// Write a finished quad to the archive file. All the offsets and
|
||||
// numbers in the *Info structs are filled in automatically based on
|
||||
// the additional data in the Holder structs.
|
||||
void writeQuad(ref QuadHolder qh)
|
||||
{
|
||||
scope auto _trc = new MTrace("writeQuad");
|
||||
|
||||
// Write the MeshInfo's first
|
||||
int meshNum = qh.meshes.length;
|
||||
|
||||
MeshInfo meshes[] = buf.write!(MeshInfo)(meshNum);
|
||||
|
||||
float minh = float.infinity;
|
||||
float maxh = -float.infinity;
|
||||
|
||||
// Then write the mesh data in approximately the order it's read
|
||||
for(int i=0; i<meshNum; i++)
|
||||
{
|
||||
assert(meshes !is null);
|
||||
|
||||
auto mh = &qh.meshes[i];
|
||||
|
||||
// Copy the basic data first
|
||||
meshes[i] = mh.info;
|
||||
|
||||
minh = min(minh,mh.info.minHeight);
|
||||
maxh = max(maxh,mh.info.maxHeight);
|
||||
|
||||
// Set everything else except the offsets
|
||||
int alphaNum = mh.alphas.length;
|
||||
meshes[i].alphaNum = alphaNum;
|
||||
|
||||
// Write the vertex buffer
|
||||
meshes[i].vertBufOffset = buf.size;
|
||||
meshes[i].vertBufSize = mh.vertexBuffer.length;
|
||||
writeBuf(mh.vertexBuffer);
|
||||
assert(buf.size == meshes[i].vertBufOffset + meshes[i].vertBufSize);
|
||||
|
||||
// Next write the alpha maps, if any
|
||||
meshes[i].alphaOffset = buf.size;
|
||||
AlphaInfo ais[] = buf.write!(AlphaInfo)(alphaNum);
|
||||
|
||||
// Loop through the alpha maps
|
||||
foreach(int k, ref ai; ais)
|
||||
{
|
||||
AlphaHolder ah = mh.alphas[k];
|
||||
ai = ah.info;
|
||||
|
||||
// Write the alpha pixel buffer
|
||||
ai.bufOffset = buf.size;
|
||||
ai.bufSize = ah.buffer.length;
|
||||
writeBuf(ah.buffer);
|
||||
}
|
||||
}
|
||||
// Finally set up the QuadInfo itself
|
||||
QuadInfo qi;
|
||||
|
||||
// Copy basic info
|
||||
qi = qh.info;
|
||||
|
||||
// Derived info
|
||||
qi.meshNum = meshNum;
|
||||
qi.offset = fileOffset;
|
||||
qi.size = buf.size;
|
||||
qi.minHeight = minh;
|
||||
qi.maxHeight = maxh;
|
||||
|
||||
// Get the side length, or the height difference if that is bigger
|
||||
qi.boundingRadius = max(maxh-minh,qi.worldWidth);
|
||||
|
||||
// Multiply with roughly sqrt(1/2), converts from side length to
|
||||
// radius with some extra slack
|
||||
qi.boundingRadius *= 0.8;
|
||||
|
||||
// The quad cache is done, write it to file
|
||||
buf.writeTo(mainFile);
|
||||
|
||||
// Update the main offset
|
||||
fileOffset += qi.size;
|
||||
|
||||
// Add the quad to the list. This list isn't written to the main
|
||||
// cache file, but to the index file.
|
||||
quadList ~= qi;
|
||||
}
|
||||
|
||||
// Add a texture name as a string. Will convert .tga file names to
|
||||
// .dds as a convenience. TODO: Use the resource system to do this,
|
||||
// it automatically searches for the dds variant.
|
||||
int addTexture(char[] orig)
|
||||
{
|
||||
if(orig.iEnds(".tga"))
|
||||
orig = orig[0..$-3] ~ "dds";
|
||||
return addString(orig);
|
||||
}
|
||||
|
||||
// Convert a string to an index
|
||||
int addString(char[] str)
|
||||
{
|
||||
// Do we already have the string?
|
||||
if(str in stringList)
|
||||
return stringList[str];
|
||||
|
||||
// Nope, insert it
|
||||
int index = stringList.length;
|
||||
stringList[str] = index;
|
||||
stringLookup[index] = str;
|
||||
|
||||
// Sum up the string lengths + 1 byte for the zero
|
||||
totalStringLength += str.length + 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
char[] getString(int index)
|
||||
{
|
||||
char[] res = stringLookup[index];
|
||||
assert(stringList[res] == index);
|
||||
return res;
|
||||
}
|
||||
|
||||
private:
|
||||
// Write the given block of memory to 'buf', possibly compressing
|
||||
// the data.
|
||||
void writeBuf(void[] ptr)
|
||||
{
|
||||
ulong size = ptr.length;
|
||||
|
||||
// Reserve the maximum bytes needed.
|
||||
void toPtr[] = buf.reserve(size);
|
||||
|
||||
// Store the data
|
||||
toPtr[] = ptr[];
|
||||
|
||||
// Add the result buffer
|
||||
buf.add(toPtr[0..size]);
|
||||
}
|
||||
|
||||
// Used for 'writing' to a changable memory buffer before writing to
|
||||
// file
|
||||
OutBuffer buf;
|
||||
|
||||
// Common vertex and index buffers for all quads. One buffer per
|
||||
// level.
|
||||
float[][] vertBuf;
|
||||
ushort[] indexBuf;
|
||||
|
||||
// Variables that must be set during the gen phase
|
||||
int maxLevel;
|
||||
int alphaSize;
|
||||
|
||||
// Contains a unique index for each string
|
||||
int[char[]] stringList;
|
||||
char[][int] stringLookup;
|
||||
size_t totalStringLength;
|
||||
|
||||
// List of all quads
|
||||
QuadInfo[] quadList;
|
||||
|
||||
// Output file
|
||||
File mainFile;
|
||||
size_t fileOffset;
|
||||
|
||||
// Index file name
|
||||
char[] iname;
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
class BaseLand
|
||||
{
|
||||
public:
|
||||
BaseLand()
|
||||
{
|
||||
createMesh();
|
||||
}
|
||||
|
||||
~BaseLand()
|
||||
{
|
||||
destroyMesh();
|
||||
}
|
||||
|
||||
// Repositions the mesh based on camera location
|
||||
void update()
|
||||
{
|
||||
Ogre::Real vd = mCamera->getFarClipDistance();
|
||||
// Recreate the mesh if the view distance has increased
|
||||
if ( vd > mMeshDistance )
|
||||
{
|
||||
destroyMesh();
|
||||
createMesh();
|
||||
}
|
||||
|
||||
Ogre::Vector3 p = mCamera->getDerivedPosition();
|
||||
p.x -= ((int)p.x % CELL_WIDTH);
|
||||
p.z -= ((int)p.z % CELL_WIDTH);
|
||||
|
||||
float h = (p.y + 2048)*2.0/CELL_WIDTH;
|
||||
h *= h;
|
||||
|
||||
mNode->setPosition(p.x, -p.z, -32 -h);
|
||||
}
|
||||
|
||||
private:
|
||||
void createMesh()
|
||||
{
|
||||
float vd = mCamera->getFarClipDistance();
|
||||
|
||||
mMeshDistance = vd;
|
||||
|
||||
vd = vd/CELL_WIDTH * 32;
|
||||
|
||||
mMat = Ogre::MaterialManager::getSingleton().
|
||||
create("BaseLandMat",
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
|
||||
Ogre::TextureUnitState* us = mMat->getTechnique(0)->getPass(0)->createTextureUnitState("_land_default.dds");
|
||||
us->setTextureScale(1.0f/vd,1.0f/vd);
|
||||
|
||||
mMat->getTechnique(0)->getPass(0)->setDepthBias(-1);
|
||||
|
||||
mObject = mSceneMgr->createManualObject("BaseLand");
|
||||
mObject->begin("BaseLandMat", Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
|
||||
vd = mMeshDistance;
|
||||
|
||||
const int HEIGHT = -2048 - 10;
|
||||
|
||||
mObject->position(-vd,vd,HEIGHT);
|
||||
mObject->textureCoord(0, 1);
|
||||
|
||||
mObject->position(-vd,-vd,HEIGHT);
|
||||
mObject->textureCoord(0, 0);
|
||||
|
||||
mObject->position(vd,-vd,HEIGHT);
|
||||
mObject->textureCoord(1, 0);
|
||||
|
||||
mObject->position(vd,vd,HEIGHT);
|
||||
mObject->textureCoord(1, 1);
|
||||
|
||||
mObject->quad(0,1,2,3);
|
||||
|
||||
mObject->end();
|
||||
|
||||
mNode = g_rootTerrainNode->createChildSceneNode();
|
||||
mNode->attachObject(mObject);
|
||||
}
|
||||
|
||||
void destroyMesh()
|
||||
{
|
||||
mNode->detachAllObjects();
|
||||
mSceneMgr->destroyManualObject(mObject);
|
||||
mNode->getParentSceneNode()->removeAndDestroyChild(mNode->getName());
|
||||
|
||||
mMat->getCreator()->remove(mMat->getHandle());
|
||||
mMat = Ogre::MaterialPtr();
|
||||
}
|
||||
|
||||
///the created mesh
|
||||
Ogre::ManualObject* mObject;
|
||||
|
||||
///The material for the mesh
|
||||
Ogre::MaterialPtr mMat;
|
||||
|
||||
///scene node for the mesh
|
||||
Ogre::SceneNode* mNode;
|
||||
|
||||
///In essence, the farViewDistance of the camera last frame
|
||||
Ogre::Real mMeshDistance;
|
||||
};
|
@ -1,286 +0,0 @@
|
||||
// The Ogre renderable used to hold and display the terrain meshes.
|
||||
class TerrainMesh : public Ogre::Renderable, public Ogre::MovableObject
|
||||
{
|
||||
public:
|
||||
|
||||
TerrainMesh(Ogre::SceneNode *parent, const MeshInfo &info,
|
||||
int level, float scale)
|
||||
: Ogre::Renderable(),
|
||||
Ogre::MovableObject()
|
||||
{
|
||||
TRACE("TerrainMesh()");
|
||||
|
||||
mLevel = level;
|
||||
|
||||
// This is a bit messy, with everything in one function. We could
|
||||
// split it up later.
|
||||
|
||||
// Use MW coordinates all the way
|
||||
assert(info.worldWidth > 0);
|
||||
assert(info.minHeight <= info.maxHeight);
|
||||
mBounds.setExtents(0,0,info.minHeight,
|
||||
info.worldWidth, info.worldWidth,
|
||||
info.maxHeight);
|
||||
mCenter = mBounds.getCenter();
|
||||
mBoundingRadius = mBounds.getHalfSize().length();
|
||||
|
||||
// TODO: VertexData has a clone() function. This probably means we
|
||||
// can set this up once and then clone it, to get a completely
|
||||
// unnoticable increase in performance :)
|
||||
mVertices = new VertexData();
|
||||
mVertices->vertexStart = 0;
|
||||
mVertices->vertexCount = info.vertRows*info.vertCols;
|
||||
|
||||
VertexDeclaration* vertexDecl = mVertices->vertexDeclaration;
|
||||
size_t currOffset = 0;
|
||||
|
||||
vertexDecl->addElement(0, currOffset, VET_FLOAT3, VES_POSITION);
|
||||
currOffset += VertexElement::getTypeSize(VET_FLOAT3);
|
||||
|
||||
vertexDecl->addElement(0, currOffset, VET_FLOAT3, VES_NORMAL);
|
||||
currOffset += VertexElement::getTypeSize(VET_FLOAT3);
|
||||
|
||||
vertexDecl->addElement(0, currOffset, VET_FLOAT2,
|
||||
VES_TEXTURE_COORDINATES, 0);
|
||||
currOffset += VertexElement::getTypeSize(VET_FLOAT2);
|
||||
|
||||
assert(vertexDecl->getVertexSize(0) == currOffset);
|
||||
|
||||
HardwareVertexBufferSharedPtr mMainBuffer;
|
||||
mMainBuffer = HardwareBufferManager::getSingleton().createVertexBuffer
|
||||
(
|
||||
vertexDecl->getVertexSize(0), // size of one whole vertex
|
||||
mVertices->vertexCount, // number of vertices
|
||||
HardwareBuffer::HBU_STATIC_WRITE_ONLY, // usage
|
||||
false); // no shadow buffer
|
||||
|
||||
// Bind the data
|
||||
mVertices->vertexBufferBinding->setBinding(0, mMainBuffer);
|
||||
|
||||
// Fill the buffer
|
||||
float* verts = static_cast<float*>
|
||||
(mMainBuffer->lock(HardwareBuffer::HBL_DISCARD));
|
||||
info.fillVertexBuffer(verts,8*mVertices->vertexCount);
|
||||
mMainBuffer->unlock();
|
||||
|
||||
// Create the index data holder
|
||||
mIndices = new IndexData();
|
||||
mIndices->indexCount = 64*64*6; // TODO: Shouldn't be hard-coded
|
||||
mIndices->indexBuffer =
|
||||
HardwareBufferManager::getSingleton().createIndexBuffer
|
||||
( HardwareIndexBuffer::IT_16BIT,
|
||||
mIndices->indexCount,
|
||||
HardwareBuffer::HBU_STATIC_WRITE_ONLY,
|
||||
false);
|
||||
|
||||
// Fill the buffer with warm fuzzy archive data
|
||||
unsigned short* indices = static_cast<unsigned short*>
|
||||
(mIndices->indexBuffer->lock
|
||||
(0, mIndices->indexBuffer->getSizeInBytes(),
|
||||
HardwareBuffer::HBL_DISCARD));
|
||||
info.fillIndexBuffer(indices,mIndices->indexCount);
|
||||
mIndices->indexBuffer->unlock();
|
||||
|
||||
// Finally, create the material
|
||||
const std::string texName = info.getTexName();
|
||||
|
||||
// Set up the scene node.
|
||||
mNode = parent->createChildSceneNode();
|
||||
mNode->attachObject(this);
|
||||
|
||||
// Finally, create or retrieve the material
|
||||
if(MaterialManager::getSingleton().resourceExists(texName))
|
||||
{
|
||||
mMaterial = MaterialManager::getSingleton().getByName
|
||||
(texName);
|
||||
return;
|
||||
}
|
||||
|
||||
// No existing material. Create a new one.
|
||||
mMaterial = MaterialManager::getSingleton().create
|
||||
(texName, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
|
||||
Pass* pass = mMaterial->getTechnique(0)->getPass(0);
|
||||
pass->setLightingEnabled(false);
|
||||
|
||||
if(level > 1)
|
||||
{
|
||||
// This material just has a normal texture
|
||||
pass->createTextureUnitState(texName)
|
||||
//->setTextureAddressingMode(TextureUnitState::TAM_CLAMP)
|
||||
;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(level == 1);
|
||||
|
||||
// Get the background texture. TODO: We should get this from
|
||||
// somewhere, no file names should be hard coded. The texture
|
||||
// might exist as a .tga in earlier versions of the game, and
|
||||
// we might also want to specify a different background
|
||||
// texture on some meshes.
|
||||
//const char *bgTex = info.getBackgroundTex();
|
||||
|
||||
const char *bgTex = "_land_default.dds";
|
||||
pass->createTextureUnitState(bgTex)
|
||||
->setTextureScale(scale,scale);
|
||||
|
||||
// Loop through all the textures in this mesh
|
||||
for(int tnum=0; tnum<info.alphaNum; tnum++)
|
||||
{
|
||||
const AlphaInfo &alpha = *info.getAlphaInfo(tnum);
|
||||
|
||||
// Name of the alpha map texture to create
|
||||
std::string alphaName = alpha.getAlphaName();
|
||||
|
||||
// Name of the texture
|
||||
std::string tname = alpha.getTexName();
|
||||
|
||||
// Create the alpha texture if it doesn't exist
|
||||
if(!TextureManager::getSingleton().resourceExists(alphaName))
|
||||
{
|
||||
TexturePtr texPtr = Ogre::TextureManager::
|
||||
getSingleton().createManual
|
||||
(alphaName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
|
||||
Ogre::TEX_TYPE_2D,
|
||||
g_alphaSize,g_alphaSize,
|
||||
1,0, // depth, mipmaps
|
||||
Ogre::PF_A8, // One-channel alpha
|
||||
Ogre::TU_STATIC_WRITE_ONLY);
|
||||
|
||||
// Get the pointer
|
||||
Ogre::HardwarePixelBufferSharedPtr pixelBuffer = texPtr->getBuffer();
|
||||
pixelBuffer->lock(Ogre::HardwareBuffer::HBL_DISCARD);
|
||||
const Ogre::PixelBox& pixelBox = pixelBuffer->getCurrentLock();
|
||||
Ogre::uint8* pDest = static_cast<Ogre::uint8*>(pixelBox.data);
|
||||
|
||||
// Copy alpha data from file
|
||||
alpha.fillAlphaBuffer(pDest,g_alphaSize*g_alphaSize);
|
||||
|
||||
// Close the buffer
|
||||
pixelBuffer->unlock();
|
||||
}
|
||||
|
||||
pass = mMaterial->getTechnique(0)->createPass();
|
||||
pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
|
||||
pass->setLightingEnabled(false);
|
||||
pass->setDepthFunction(Ogre::CMPF_EQUAL);
|
||||
|
||||
Ogre::TextureUnitState* tus = pass->createTextureUnitState(alphaName);
|
||||
//tus->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
|
||||
|
||||
tus->setAlphaOperation(Ogre::LBX_BLEND_TEXTURE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_TEXTURE);
|
||||
tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_TEXTURE);
|
||||
tus->setIsAlpha(true);
|
||||
|
||||
// Add the actual texture on top of the alpha map.
|
||||
tus = pass->createTextureUnitState(tname);
|
||||
tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_CURRENT);
|
||||
|
||||
tus->setTextureScale(scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~TerrainMesh()
|
||||
{
|
||||
assert(mNode);
|
||||
mNode->detachAllObjects();
|
||||
mNode->getCreator()->destroySceneNode(mNode);
|
||||
|
||||
// TODO: This still crashes on level1 meshes. Find out why!
|
||||
if(mLevel!=1)delete mVertices;
|
||||
delete mIndices;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// These are all Ogre functions that we have to override
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
// Internal Ogre function. We should call visitor->visit on all
|
||||
// Renderables that are part of this object. In our case, this is
|
||||
// only ourselves.
|
||||
void visitRenderables(Renderable::Visitor* visitor,
|
||||
bool debugRenderables = false) {
|
||||
visitor->visit(this, 0, false);
|
||||
}
|
||||
|
||||
void getRenderOperation( Ogre::RenderOperation& op ) {
|
||||
op.useIndexes = true;
|
||||
op.operationType = Ogre::RenderOperation::OT_TRIANGLE_LIST;
|
||||
op.vertexData = mVertices;
|
||||
op.indexData = mIndices;
|
||||
}
|
||||
|
||||
void getWorldTransforms( Ogre::Matrix4* xform ) const {
|
||||
*xform = mNode->_getFullTransform();
|
||||
}
|
||||
|
||||
const Ogre::Quaternion& getWorldOrientation(void) const {
|
||||
return mNode->_getDerivedOrientation();
|
||||
}
|
||||
|
||||
const Ogre::Vector3& getWorldPosition(void) const {
|
||||
return mNode->_getDerivedPosition();
|
||||
}
|
||||
|
||||
Ogre::Real getSquaredViewDepth(const Ogre::Camera *cam) const {
|
||||
Ogre::Vector3 diff = mCenter - cam->getDerivedPosition();
|
||||
// Use squared length to avoid square root
|
||||
return diff.squaredLength();
|
||||
}
|
||||
|
||||
const Ogre::LightList& getLights(void) const {
|
||||
if (mLightListDirty) {
|
||||
getParentSceneNode()->getCreator()->_populateLightList
|
||||
(mCenter, mBoundingRadius, mLightList);
|
||||
mLightListDirty = false;
|
||||
}
|
||||
return mLightList;
|
||||
}
|
||||
virtual const Ogre::String& getMovableType( void ) const {
|
||||
static Ogre::String t = "MW_TERRAIN";
|
||||
return t;
|
||||
}
|
||||
void _updateRenderQueue( Ogre::RenderQueue* queue ) {
|
||||
mLightListDirty = true;
|
||||
queue->addRenderable(this, mRenderQueueID);
|
||||
}
|
||||
const Ogre::AxisAlignedBox& getBoundingBox( void ) const
|
||||
{
|
||||
return mBounds;
|
||||
}
|
||||
|
||||
Ogre::Real getBoundingRadius(void) const {
|
||||
return mBoundingRadius;
|
||||
}
|
||||
virtual const MaterialPtr& getMaterial(void) const
|
||||
{ return mMaterial; }
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
private:
|
||||
|
||||
int mLevel;
|
||||
|
||||
Ogre::SceneNode* mNode;
|
||||
|
||||
Ogre::MaterialPtr mMaterial;
|
||||
|
||||
Ogre::VertexData* mVertices;
|
||||
Ogre::IndexData* mIndices;
|
||||
|
||||
mutable bool mLightListDirty;
|
||||
|
||||
Ogre::Real mBoundingRadius;
|
||||
Ogre::AxisAlignedBox mBounds;
|
||||
Ogre::Vector3 mCenter;
|
||||
};
|
@ -1,413 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2009 Jacob Essex, Nicolay Korslund
|
||||
WWW: http://openmw.sourceforge.net/
|
||||
|
||||
This file (cpp_terrain.cpp) 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/ .
|
||||
|
||||
*/
|
||||
|
||||
const int CELL_WIDTH = 8192;
|
||||
|
||||
SceneNode *g_rootTerrainNode;
|
||||
int g_alphaSize;
|
||||
|
||||
struct MeshInfo;
|
||||
struct AlphaInfo;
|
||||
|
||||
// D functions
|
||||
extern "C"
|
||||
{
|
||||
void d_terr_superman();
|
||||
void d_terr_terrainUpdate();
|
||||
|
||||
char *d_terr_getTexName(int32_t);
|
||||
|
||||
void d_terr_fillVertexBuffer(const MeshInfo*,float*,uint64_t);
|
||||
void d_terr_fillIndexBuffer(const MeshInfo*,uint16_t*,uint64_t);
|
||||
AlphaInfo *d_terr_getAlphaInfo(const MeshInfo*,int32_t);
|
||||
|
||||
void d_terr_fillAlphaBuffer(const AlphaInfo*,uint8_t*,uint64_t);
|
||||
|
||||
int32_t d_terr_getAlphaSize();
|
||||
}
|
||||
|
||||
// Info about a submesh. This is a clone of the struct defined in
|
||||
// archive.d. TODO: Make sure the D and C++ structs are of the same
|
||||
// size and alignment.
|
||||
struct MeshInfo
|
||||
{
|
||||
// Bounding box info
|
||||
float minHeight, maxHeight;
|
||||
float worldWidth;
|
||||
|
||||
// Vertex and index numbers
|
||||
int32_t vertRows, vertCols;
|
||||
|
||||
// Height offset to apply to all vertices
|
||||
float heightOffset;
|
||||
|
||||
// Size and offset of the vertex buffer
|
||||
int64_t vertBufSize, vertBufOffset;
|
||||
|
||||
// Texture name. Index to the string table.
|
||||
int32_t texName;
|
||||
|
||||
// Number and offset of AlphaInfo blocks
|
||||
int32_t alphaNum;
|
||||
uint64_t alphaOffset;
|
||||
|
||||
inline void fillVertexBuffer(float *buffer, uint64_t size) const
|
||||
{
|
||||
d_terr_fillVertexBuffer(this, buffer, size);
|
||||
}
|
||||
|
||||
inline void fillIndexBuffer(uint16_t *buffer, uint64_t size) const
|
||||
{
|
||||
d_terr_fillIndexBuffer(this, buffer, size);
|
||||
}
|
||||
|
||||
inline char* getTexName() const
|
||||
{
|
||||
return d_terr_getTexName(texName);
|
||||
}
|
||||
|
||||
inline AlphaInfo *getAlphaInfo(int tnum) const
|
||||
{
|
||||
return d_terr_getAlphaInfo(this, tnum);
|
||||
}
|
||||
};
|
||||
|
||||
// Info about an alpha map belonging to a mesh
|
||||
struct AlphaInfo
|
||||
{
|
||||
// Position of the actual image data
|
||||
uint64_t bufSize, bufOffset;
|
||||
|
||||
// The texture name for this layer. The actual string is stored in
|
||||
// the archive's string buffer.
|
||||
int32_t texName;
|
||||
int32_t alphaName;
|
||||
|
||||
inline char* getTexName() const
|
||||
{
|
||||
return d_terr_getTexName(texName);
|
||||
}
|
||||
|
||||
inline char* getAlphaName() const
|
||||
{
|
||||
return d_terr_getTexName(alphaName);
|
||||
}
|
||||
|
||||
inline void fillAlphaBuffer(uint8_t *buffer, uint64_t size) const
|
||||
{
|
||||
return d_terr_fillAlphaBuffer(this, buffer, size);
|
||||
}
|
||||
};
|
||||
|
||||
#include "cpp_baseland.cpp"
|
||||
#include "cpp_mesh.cpp"
|
||||
|
||||
BaseLand *g_baseLand;
|
||||
|
||||
class TerrainFrameListener : public FrameListener
|
||||
{
|
||||
protected:
|
||||
bool frameEnded(const FrameEvent& evt)
|
||||
{
|
||||
TRACE("Terrain frame");
|
||||
d_terr_terrainUpdate();
|
||||
if(g_baseLand)
|
||||
g_baseLand->update();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Renders a material into a texture
|
||||
Ogre::TexturePtr getRenderedTexture(Ogre::MaterialPtr mp,
|
||||
const std::string& name,
|
||||
int texSize, Ogre::PixelFormat tt)
|
||||
{
|
||||
Ogre::CompositorPtr cp = Ogre::CompositorManager::getSingleton().
|
||||
create("Rtt_Comp",
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
|
||||
Ogre::CompositionTargetPass* ctp = cp->createTechnique()->getOutputTargetPass();
|
||||
Ogre::CompositionPass* cpass = ctp->createPass();
|
||||
cpass->setType(Ogre::CompositionPass::PT_RENDERQUAD);
|
||||
cpass->setMaterial(mp);
|
||||
|
||||
// Create the destination texture
|
||||
Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().
|
||||
createManual(name + "_T",
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
|
||||
Ogre::TEX_TYPE_2D,
|
||||
texSize,
|
||||
texSize,
|
||||
0,
|
||||
tt,
|
||||
Ogre::TU_RENDERTARGET
|
||||
);
|
||||
|
||||
Ogre::RenderTexture* renderTexture = texture->getBuffer()->getRenderTarget();
|
||||
Ogre::Viewport* vp = renderTexture->addViewport(mCamera);
|
||||
|
||||
Ogre::CompositorManager::getSingleton().addCompositor(vp, "Rtt_Comp");
|
||||
Ogre::CompositorManager::getSingleton().setCompositorEnabled(vp,"Rtt_Comp", true);
|
||||
|
||||
renderTexture->update();
|
||||
|
||||
// Call the OGRE renderer.
|
||||
Ogre::Root::getSingleton().renderOneFrame();
|
||||
|
||||
Ogre::CompositorManager::getSingleton().removeCompositor(vp, "Rtt_Comp");
|
||||
Ogre::CompositorManager::getSingleton().remove(cp->getHandle());
|
||||
|
||||
renderTexture->removeAllViewports();
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
// These are used between some functions below. Kinda messy. Since
|
||||
// these are GLOBAL instances, they are terminated at program
|
||||
// exit. However, OGRE itself is terminated before that, so we have to
|
||||
// make sure we have no 'active' shared pointers after OGRE is
|
||||
// finished (otherwise we get a segfault at exit.)
|
||||
std::list<Ogre::ResourcePtr> createdResources;
|
||||
Ogre::HardwarePixelBuffer *pixelBuffer;
|
||||
MaterialPtr mat;
|
||||
|
||||
// Functions called from D
|
||||
extern "C"
|
||||
{
|
||||
SceneNode* terr_createChildNode(float x, float y,
|
||||
SceneNode *parent)
|
||||
{
|
||||
Ogre::Vector3 pos(x,y,0);
|
||||
if(parent == NULL)
|
||||
parent = g_rootTerrainNode;
|
||||
|
||||
assert(parent);
|
||||
return parent->createChildSceneNode(pos);
|
||||
}
|
||||
|
||||
void terr_destroyNode(SceneNode *node)
|
||||
{
|
||||
node->removeAndDestroyAllChildren();
|
||||
mSceneMgr->destroySceneNode(node);
|
||||
}
|
||||
|
||||
// TODO: We could make allocation a little more refined than new and
|
||||
// delete. But that's true for everything here. A freelist based
|
||||
// approach is best in most of these cases, as we have continuous
|
||||
// allocation/deallocation of fixed-size structs.
|
||||
Ogre::AxisAlignedBox *terr_makeBounds(float minHeight, float maxHeight,
|
||||
float width, SceneNode* node)
|
||||
{
|
||||
TRACE("terr_makeBounds");
|
||||
AxisAlignedBox *mBounds = new AxisAlignedBox;
|
||||
|
||||
assert(maxHeight >= minHeight);
|
||||
|
||||
mBounds->setExtents(0,0,minHeight,
|
||||
width,width,maxHeight);
|
||||
|
||||
// Transform the box to world coordinates, so it can be compared
|
||||
// with the camera later.
|
||||
mBounds->transformAffine(node->_getFullTransform());
|
||||
|
||||
return mBounds;
|
||||
}
|
||||
|
||||
void terr_killBounds(AxisAlignedBox *bounds)
|
||||
{
|
||||
TRACE("terr_killBounds");
|
||||
delete bounds;
|
||||
}
|
||||
|
||||
float terr_getSqCamDist(AxisAlignedBox *mBounds)
|
||||
{
|
||||
TRACE("terr_getSqCamDist");
|
||||
Ogre::Vector3 cpos = mCamera->getDerivedPosition();
|
||||
Ogre::Vector3 diff(0, 0, 0);
|
||||
diff.makeFloor(cpos - mBounds->getMinimum() );
|
||||
diff.makeCeil(cpos - mBounds->getMaximum() );
|
||||
return diff.squaredLength();
|
||||
}
|
||||
|
||||
TerrainMesh *terr_makeMesh(SceneNode *parent,
|
||||
MeshInfo *info,
|
||||
int level, float scale)
|
||||
{
|
||||
return new TerrainMesh(parent, *info, level, scale);
|
||||
}
|
||||
|
||||
void terr_killMesh(TerrainMesh *mesh)
|
||||
{
|
||||
TRACE("terr_killMesh");
|
||||
delete mesh;
|
||||
}
|
||||
|
||||
// Set up the rendering system
|
||||
void terr_setupRendering()
|
||||
{
|
||||
TRACE("terr_setupRendering()");
|
||||
// Make sure the C++ sizes match the D sizes, since the structs
|
||||
// are shared between the two.
|
||||
assert(sizeof(MeshInfo) == 14*4);
|
||||
assert(sizeof(AlphaInfo) == 6*4);
|
||||
|
||||
// Add the terrain directory as a resource location. TODO: Get the
|
||||
// name from D.
|
||||
ResourceGroupManager::getSingleton().
|
||||
addResourceLocation("cache/terrain/", "FileSystem", "General");
|
||||
|
||||
// Enter superman mode
|
||||
mCamera->setFarClipDistance(40*CELL_WIDTH);
|
||||
//ogre_setFog(0.7, 0.7, 0.7, 200, 32*CELL_WIDTH);
|
||||
d_terr_superman();
|
||||
|
||||
// Create a root scene node first. The 'root' node is rotated to
|
||||
// match the MW coordinate system
|
||||
g_rootTerrainNode = mwRoot->createChildSceneNode("TERRAIN_ROOT");
|
||||
|
||||
// Add the base land. This is the ground beneath the actual
|
||||
// terrain mesh that makes the terrain look infinite.
|
||||
//g_baseLand = new BaseLand();
|
||||
|
||||
g_alphaSize = d_terr_getAlphaSize();
|
||||
|
||||
// Add the frame listener
|
||||
mRoot->addFrameListener(new TerrainFrameListener);
|
||||
}
|
||||
|
||||
// The next four functions are called in the function genLevel2Map()
|
||||
// only. This is very top-down-programming-ish and a bit messy, but
|
||||
// that's what I get for mixing C++ and D like this.
|
||||
void terr_makeLandMaterial(const char* name, float scale)
|
||||
{
|
||||
// Get a new material
|
||||
mat = Ogre::MaterialManager::getSingleton().
|
||||
create(name,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
|
||||
// Put the default texture in the bottom 'layer', so that we don't
|
||||
// end up seeing through the landscape.
|
||||
Ogre::Pass* np = mat->getTechnique(0)->getPass(0);
|
||||
np->setLightingEnabled(false);
|
||||
np->createTextureUnitState("_land_default.dds")
|
||||
->setTextureScale(scale,scale);
|
||||
}
|
||||
|
||||
uint8_t *terr_makeAlphaLayer(const char* name, int32_t width)
|
||||
{
|
||||
// Create alpha map for this texture.
|
||||
Ogre::TexturePtr texPtr = Ogre::TextureManager::getSingleton().
|
||||
createManual(name,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
|
||||
Ogre::TEX_TYPE_2D,
|
||||
width, width,
|
||||
1,0, // depth, mipmaps
|
||||
Ogre::PF_A8, // One-channel alpha
|
||||
Ogre::TU_STATIC_WRITE_ONLY);
|
||||
|
||||
createdResources.push_back(texPtr);
|
||||
|
||||
pixelBuffer = texPtr->getBuffer().get();
|
||||
pixelBuffer->lock(Ogre::HardwareBuffer::HBL_DISCARD);
|
||||
const Ogre::PixelBox& pixelBox = pixelBuffer->getCurrentLock();
|
||||
|
||||
return static_cast<Ogre::uint8*>(pixelBox.data);
|
||||
}
|
||||
|
||||
void terr_closeAlpha(const char *alphaName,
|
||||
const char *texName, float scale)
|
||||
{
|
||||
// Close the alpha pixel buffer opened in the previous function
|
||||
pixelBuffer->unlock();
|
||||
|
||||
// Create a pass containing the alpha map
|
||||
Pass *np = mat->getTechnique(0)->createPass();
|
||||
np->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
|
||||
np->setLightingEnabled(false);
|
||||
np->setDepthFunction(Ogre::CMPF_EQUAL);
|
||||
Ogre::TextureUnitState* tus = np->createTextureUnitState(alphaName);
|
||||
tus->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
|
||||
|
||||
// Set various blending options
|
||||
tus->setAlphaOperation(Ogre::LBX_BLEND_TEXTURE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_TEXTURE);
|
||||
tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_TEXTURE);
|
||||
tus->setIsAlpha(true);
|
||||
|
||||
// Add the terrain texture to the pass and scale it.
|
||||
tus = np->createTextureUnitState(texName);
|
||||
tus->setColourOperationEx(Ogre::LBX_BLEND_DIFFUSE_ALPHA,
|
||||
Ogre::LBS_TEXTURE,
|
||||
Ogre::LBS_CURRENT);
|
||||
tus->setTextureScale(scale, scale);
|
||||
}
|
||||
|
||||
// Clean up after the above functions, render the material to
|
||||
// texture and save the data in outdata and in the file outname.
|
||||
void terr_cleanupAlpha(const char *outname,
|
||||
void *outData, int32_t toSize)
|
||||
{
|
||||
TexturePtr tex1 = getRenderedTexture(mat,outname,
|
||||
toSize,Ogre::PF_R8G8B8);
|
||||
|
||||
// Blit the texture into the given memory buffer
|
||||
PixelBox pb = PixelBox(toSize, toSize, 1, PF_R8G8B8);
|
||||
pb.data = outData;
|
||||
tex1->getBuffer()->blitToMemory(pb);
|
||||
|
||||
// Clean up
|
||||
TextureManager::getSingleton().remove(tex1->getHandle());
|
||||
const std::list<Ogre::ResourcePtr>::const_iterator iend = createdResources.end();
|
||||
for ( std::list<Ogre::ResourcePtr>::const_iterator itr = createdResources.begin();
|
||||
itr != iend;
|
||||
++itr)
|
||||
(*itr)->getCreator()->remove((*itr)->getHandle());
|
||||
createdResources.clear();
|
||||
|
||||
MaterialManager::getSingleton().remove(mat->getHandle());
|
||||
mat.setNull();
|
||||
}
|
||||
|
||||
void terr_resize(void* srcPtr, void* dstPtr, int32_t fromW, int32_t toW)
|
||||
{
|
||||
// Create pixelboxes
|
||||
PixelBox src = PixelBox(fromW, fromW, 1, PF_R8G8B8);
|
||||
PixelBox dst = PixelBox(toW, toW, 1, PF_R8G8B8);
|
||||
|
||||
src.data = srcPtr;
|
||||
dst.data = dstPtr;
|
||||
|
||||
// Resize the image. The nearest neighbour filter makes sure
|
||||
// there is no blurring.
|
||||
Image::scale(src, dst, Ogre::Image::FILTER_NEAREST);
|
||||
}
|
||||
|
||||
void terr_saveImage(void *data, int32_t width, const char* name)
|
||||
{
|
||||
Image img;
|
||||
img.loadDynamicImage((uchar*)data, width, width, PF_R8G8B8);
|
||||
img.save(name);
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
module terrain.esmland;
|
||||
|
||||
import esm.loadltex;
|
||||
import esm.loadcell;
|
||||
import util.regions;
|
||||
import esm.filereader;
|
||||
|
||||
import std.stdio;
|
||||
|
||||
const int LAND_NUM_VERTS = 65*65;
|
||||
|
||||
MWLand mwland;
|
||||
|
||||
// Interface to the ESM landscape data
|
||||
struct MWLand
|
||||
{
|
||||
RegionManager reg;
|
||||
|
||||
// These structs/types represent the way data is actually stored in
|
||||
// the ESM files.
|
||||
|
||||
// Heightmap
|
||||
align(1)
|
||||
struct VHGT
|
||||
{
|
||||
float heightOffset;
|
||||
byte heightData[LAND_NUM_VERTS];
|
||||
short unknown1;
|
||||
char unknown2;
|
||||
}
|
||||
|
||||
// Normals
|
||||
typedef byte[LAND_NUM_VERTS*3] VNML;
|
||||
|
||||
// Land textures. This is organized in 4x4 buffers of 4x4 squares
|
||||
// each. This is how the original engine splits up the cell meshes,
|
||||
// and it's probably a good idea for us to do the same.
|
||||
typedef short[4][4][4][4] VTEX;
|
||||
|
||||
static assert(VHGT.sizeof == 4232);
|
||||
static assert(VNML.sizeof == 12675);
|
||||
static assert(VTEX.sizeof == 512);
|
||||
|
||||
// Landscape data for one cell
|
||||
struct LandData
|
||||
{
|
||||
VHGT vhgt;
|
||||
VNML normals;
|
||||
}
|
||||
|
||||
// Texture data for one cell
|
||||
struct LTEXData
|
||||
{
|
||||
// Global list of land textures from the source ES file
|
||||
LandTextureList.TextureList source;
|
||||
|
||||
// Texture indices for this cell
|
||||
VTEX vtex;
|
||||
|
||||
// Get the texture x2,y2 from the sub map x1,x2
|
||||
char[] getTexture(int x1, int y1, int x2, int y2)
|
||||
{
|
||||
// Get the texture index relative to the current esm/esp file
|
||||
short texID = vtex[y1][x1][y2][x2] - 1;
|
||||
|
||||
if(texID == -1)
|
||||
return null;
|
||||
|
||||
// Return the 'new' texture name. This name was automatically
|
||||
// been converted to .dds at load time if the .tga file was not
|
||||
// found.
|
||||
assert(source !is null);
|
||||
assert(texID >= 0 && texID < source.length);
|
||||
return source[texID].getNewName();
|
||||
}
|
||||
|
||||
// Get a texture from the 16x16 grid in one cell
|
||||
char[] getTexture(int x, int y)
|
||||
{
|
||||
return getTexture(x/4,y/4,x%4,y%4);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the maximum absolute coordinate value in any direction
|
||||
int getMaxCoord()
|
||||
{ return cells.maxXY; }
|
||||
|
||||
// Does the given cell exist and does it have land data?
|
||||
bool hasData(int x, int y)
|
||||
{
|
||||
// Does the cell exist?
|
||||
if(!cells.hasExt(x,y))
|
||||
return false;
|
||||
|
||||
// And does it have terrain data?
|
||||
auto ex = cells.getExt(x,y);
|
||||
return ex.hasLand();
|
||||
}
|
||||
|
||||
LandData *getLandData(int x, int y)
|
||||
{
|
||||
loadCell(x, y);
|
||||
return ¤tLand;
|
||||
}
|
||||
|
||||
LTEXData *getLTEXData(int x, int y)
|
||||
{
|
||||
loadCell(x, y);
|
||||
return ¤tLtex;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
int currentX = -1234;
|
||||
int currentY = 4321;
|
||||
|
||||
LandData currentLand;
|
||||
LTEXData currentLtex;
|
||||
|
||||
// Make sure the given cell is loaded
|
||||
void loadCell(int x, int y)
|
||||
{
|
||||
// If the right cell is already loaded, don't do anything
|
||||
if(x == currentX && y == currentY)
|
||||
return;
|
||||
|
||||
assert(hasData(x,y));
|
||||
|
||||
currentX = x;
|
||||
currentY = y;
|
||||
|
||||
// Get the file context for the terrain data. This can be used to
|
||||
// skip to the right part of the ESM file.
|
||||
auto cont = cells.getExt(x,y).land.context;
|
||||
|
||||
// Get the land texture list from the file
|
||||
currentLtex.source = landTextures.files[cont.filename];
|
||||
|
||||
// We should use an existing region later, or at least delete this
|
||||
// once we're done with the gen process.
|
||||
if(reg is null)
|
||||
reg = new RegionManager();
|
||||
|
||||
// Open the ESM at this cell
|
||||
esFile.restoreContext(cont, reg);
|
||||
|
||||
// Store the cell-specific data
|
||||
esFile.readHNExact(currentLand.normals.ptr,
|
||||
currentLand.normals.length, "VNML");
|
||||
esFile.readHNExact(¤tLand.vhgt, VHGT.sizeof, "VHGT");
|
||||
|
||||
// These aren't used yet
|
||||
if(esFile.isNextSub("WNAM")) esFile.skipHSubSize(81);
|
||||
if(esFile.isNextSub("VCLR")) esFile.skipHSubSize(12675);
|
||||
|
||||
esFile.readHNExact(¤tLtex.vtex, VTEX.sizeof, "VTEX");
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,71 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2009 Nicolay Korslund
|
||||
WWW: http://openmw.sourceforge.net/
|
||||
|
||||
This file (archive.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 terrain.myfile;
|
||||
|
||||
import std.stream;
|
||||
import std.stdio;
|
||||
|
||||
// Add a couple of helper functions to the file stream
|
||||
class MyFile : File
|
||||
{
|
||||
this(string filename, FileMode mode = FileMode.In)
|
||||
{
|
||||
super(filename, mode);
|
||||
}
|
||||
|
||||
void fill(T)(ref T t)
|
||||
{
|
||||
readExact(&t, T.sizeof);
|
||||
}
|
||||
|
||||
void dump(T)(ref T t)
|
||||
{
|
||||
writeExact(&t, T.sizeof);
|
||||
}
|
||||
|
||||
void fillArray(T)(T[] t)
|
||||
{
|
||||
readExact(t.ptr, t.length*T.sizeof);
|
||||
}
|
||||
|
||||
void dumpArray(T)(T[] t)
|
||||
{
|
||||
writeExact(t.ptr, t.length*T.sizeof);
|
||||
}
|
||||
|
||||
void readArray(T)(ref T[] arr)
|
||||
{
|
||||
int size;
|
||||
read(size);
|
||||
assert(size < 1024*1024 && size > 0);
|
||||
arr = new T[size];
|
||||
fillArray!(T)(arr);
|
||||
}
|
||||
|
||||
void writeArray(T)(T[] t)
|
||||
{
|
||||
int size = t.length;
|
||||
write(size);
|
||||
dumpArray!(T)(t);
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008-2009 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.sourceforge.net/
|
||||
|
||||
This file (outbuffer.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/ .
|
||||
*/
|
||||
|
||||
/*
|
||||
This files provides a simple buffer class used for writing the cache
|
||||
files. It lets you 'write' data to a growing memory buffer and
|
||||
allows you to change the written data after the fact (since it's
|
||||
retained in memory.) When you're done, you can write the entire
|
||||
buffer to a stream in one operation.
|
||||
*/
|
||||
|
||||
module terrain.outbuffer;
|
||||
|
||||
import util.regions;
|
||||
import std.stream;
|
||||
|
||||
class OutBuffer
|
||||
{
|
||||
private:
|
||||
RegionManager reg;
|
||||
long used;
|
||||
void[][] buffers;
|
||||
|
||||
public:
|
||||
this()
|
||||
{
|
||||
reg = new RegionManager("Outbuf", 200*1024);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
if(buffers.length)
|
||||
delete buffers;
|
||||
|
||||
reg.freeAll();
|
||||
used = 0;
|
||||
buffers = null;
|
||||
}
|
||||
|
||||
// Write everyting to a stream as one buffer
|
||||
void writeTo(Stream str)
|
||||
{
|
||||
foreach(void[] v; buffers)
|
||||
str.writeExact(v.ptr, v.length);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
// Get a pointer to a new block at least 'bytes' large, but don't
|
||||
// add it to the list.
|
||||
void[] reserve(size_t bytes)
|
||||
{ return reg.allocate(bytes); }
|
||||
|
||||
// Get a new block which is 'bytes' size large.
|
||||
void[] add(size_t bytes)
|
||||
{
|
||||
void[] p = reserve(bytes);
|
||||
add(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Add an existing block to the write list
|
||||
void add(void[] p)
|
||||
{
|
||||
buffers ~= p;
|
||||
used += p.length;
|
||||
}
|
||||
|
||||
T[] write(T)(size_t num)
|
||||
{
|
||||
return cast(T[])add(num * T.sizeof);
|
||||
}
|
||||
|
||||
size_t size() { return used; }
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
module terrain.quad;
|
||||
|
||||
import terrain.archive;
|
||||
import terrain.bindings;
|
||||
import std.stdio;
|
||||
import monster.vm.dbg;
|
||||
|
||||
const int CELL_WIDTH = 8192;
|
||||
const float SPLIT_FACTOR = 0.5;
|
||||
const float UNSPLIT_FACTOR = 2.0;
|
||||
|
||||
class Quad
|
||||
{
|
||||
this(int cellX=0, int cellY=0, Quad parent = null)
|
||||
{
|
||||
scope auto _trc = new MTrace("Quad.this");
|
||||
|
||||
mCellX = cellX;
|
||||
mCellY = cellY;
|
||||
|
||||
// Do we have a parent?
|
||||
if(parent !is null)
|
||||
{
|
||||
mLevel = parent.mLevel-1;
|
||||
|
||||
// Coordinates relative to our parent
|
||||
int relX = cellX - parent.mCellX;
|
||||
int relY = cellY - parent.mCellY;
|
||||
|
||||
// The coordinates give the top left corner of the quad, or our
|
||||
// relative coordinates within that should always be positive.
|
||||
assert(relX >= 0);
|
||||
assert(relY >= 0);
|
||||
|
||||
// Create a child scene node. The scene node position is given in
|
||||
// world units, ie. CELL_WIDTH units per cell.
|
||||
mNode = terr_createChildNode(relX*CELL_WIDTH,
|
||||
relY*CELL_WIDTH,
|
||||
parent.mNode);
|
||||
|
||||
// Get the archive data for this quad.
|
||||
mInfo = g_archive.getQuad(mCellX,mCellY,mLevel);
|
||||
|
||||
// Set up the bounding box. Use MW coordinates all the
|
||||
// way.
|
||||
mBounds = terr_makeBounds(mInfo.minHeight,
|
||||
mInfo.maxHeight,
|
||||
mInfo.worldWidth,
|
||||
mNode);
|
||||
|
||||
float radius = mInfo.boundingRadius;
|
||||
|
||||
mSplitDistance = radius * SPLIT_FACTOR;
|
||||
mUnsplitDistance = radius * UNSPLIT_FACTOR;
|
||||
|
||||
// Square the distances
|
||||
mSplitDistance *= mSplitDistance;
|
||||
mUnsplitDistance *= mUnsplitDistance;
|
||||
|
||||
if(mLevel == 1)
|
||||
{
|
||||
// Create the terrain and leave it there.
|
||||
buildTerrain();
|
||||
isStatic = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No parent, this is the top-most quad. Get all the info from
|
||||
// the archive.
|
||||
mInfo = g_archive.rootQuad;
|
||||
assert(mInfo);
|
||||
|
||||
mLevel = mInfo.level;
|
||||
cellX = mCellX = mInfo.cellX;
|
||||
cellY = mCellY = mInfo.cellY;
|
||||
|
||||
mNode = terr_createChildNode(cellX*CELL_WIDTH,
|
||||
cellY*CELL_WIDTH,
|
||||
null);
|
||||
|
||||
// Split up
|
||||
split();
|
||||
|
||||
// The root can never be unsplit
|
||||
isStatic = true;
|
||||
}
|
||||
|
||||
assert(mLevel >= 1);
|
||||
assert(mNode !is null);
|
||||
|
||||
// Update the terrain. This will create the mesh or children if
|
||||
// necessary.
|
||||
update();
|
||||
}
|
||||
|
||||
~this()
|
||||
{
|
||||
scope auto _trc = new MTrace("Quad.~this");
|
||||
|
||||
// TODO: We might rewrite the code so that the quads are never
|
||||
// actually destroyed, just 'inactivated' by hiding their scene
|
||||
// node. We only call update on our children if we don't have a
|
||||
// mesh ourselves.
|
||||
if(hasMesh)
|
||||
destroyTerrain();
|
||||
else if(hasChildren)
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
delete mChildren[i];
|
||||
|
||||
terr_destroyNode(mNode);
|
||||
if(mBounds !is null)
|
||||
terr_killBounds(mBounds);
|
||||
}
|
||||
|
||||
// Remove the landscape for this quad, and create children.
|
||||
void split()
|
||||
{
|
||||
scope auto _trc = new MTrace("split");
|
||||
// Never split a static quad or a quad that already has children.
|
||||
assert(!isStatic);
|
||||
assert(!hasChildren);
|
||||
assert(mLevel > 1);
|
||||
|
||||
if(hasMesh)
|
||||
destroyTerrain();
|
||||
|
||||
// Find the cell width of our children
|
||||
int cWidth = 1 << (mLevel-2);
|
||||
|
||||
// Create children
|
||||
for ( size_t i = 0; i < 4; ++i )
|
||||
{
|
||||
if(!mInfo.hasChild[i])
|
||||
continue;
|
||||
|
||||
// The cell coordinates for this child quad
|
||||
int x = (i%2)*cWidth + mCellX;
|
||||
int y = (i/2)*cWidth + mCellY;
|
||||
|
||||
mChildren[i] = new Quad(x,y,this);
|
||||
}
|
||||
hasChildren = true;
|
||||
}
|
||||
|
||||
// Removes children and rebuilds terrain
|
||||
void unsplit()
|
||||
{
|
||||
scope auto _trc = new MTrace("unsplit");
|
||||
// Never unsplit the root quad
|
||||
assert(mLevel < g_archive.rootQuad.level);
|
||||
// Never unsplit a static or quad that isn't split.
|
||||
assert(!isStatic);
|
||||
assert(hasChildren);
|
||||
assert(!hasMesh);
|
||||
|
||||
for( size_t i = 0; i < 4; i++ )
|
||||
{
|
||||
delete mChildren[i];
|
||||
mChildren[i] = null;
|
||||
}
|
||||
|
||||
buildTerrain();
|
||||
|
||||
hasChildren = false;
|
||||
}
|
||||
|
||||
// Determines whether to split or unsplit the quad, and immediately
|
||||
// does it.
|
||||
void update()
|
||||
{
|
||||
scope auto _trc = new MTrace("Quad.update()");
|
||||
|
||||
// Static quads don't change
|
||||
if(!isStatic)
|
||||
{
|
||||
assert(mUnsplitDistance > mSplitDistance);
|
||||
|
||||
// Get (squared) camera distance. TODO: shouldn't this just
|
||||
// be a simple vector difference from the mesh center?
|
||||
assert(mBounds !is null);
|
||||
float camDist = terr_getSqCamDist(mBounds);
|
||||
|
||||
// No children?
|
||||
if(!hasChildren)
|
||||
{
|
||||
// If we're close, split now.
|
||||
if(camDist < mSplitDistance)
|
||||
split();
|
||||
else
|
||||
{
|
||||
// We're not close, and don't have any children. Should we
|
||||
// built terrain?
|
||||
if(!hasMesh)
|
||||
buildTerrain();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we either had children when we entered,
|
||||
// or we just performed a split.
|
||||
assert(!hasMesh);
|
||||
assert(hasChildren);
|
||||
|
||||
// If the camera is too far away, kill the children.
|
||||
if(camDist > mUnsplitDistance)
|
||||
{
|
||||
unsplit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(!hasChildren)
|
||||
return;
|
||||
|
||||
// We have children and we're happy about it. Update them too.
|
||||
for(int i; i < 4; ++i)
|
||||
{
|
||||
Quad q = mChildren[i];
|
||||
if(q !is null) q.update();
|
||||
}
|
||||
}
|
||||
|
||||
// Build the terrain for this quad
|
||||
void buildTerrain()
|
||||
{
|
||||
scope auto _trc = new MTrace("buildTerrain");
|
||||
|
||||
assert(!hasMesh);
|
||||
assert(!isStatic);
|
||||
|
||||
// Map the terrain data into memory.
|
||||
assert(mInfo);
|
||||
g_archive.mapQuad(mInfo);
|
||||
|
||||
// Create one mesh for each segment in the quad. TerrainMesh takes
|
||||
// care of the loading.
|
||||
meshList.length = mInfo.meshNum;
|
||||
foreach(i, ref m; meshList)
|
||||
{
|
||||
MeshInfo *mi = g_archive.getMeshInfo(i);
|
||||
m = terr_makeMesh(mNode, mi, mInfo.level, TEX_SCALE);
|
||||
}
|
||||
|
||||
hasMesh = true;
|
||||
}
|
||||
|
||||
void destroyTerrain()
|
||||
{
|
||||
scope auto _trc = new MTrace("destroyTerrain");
|
||||
|
||||
assert(hasMesh);
|
||||
|
||||
foreach(m; meshList)
|
||||
terr_killMesh(m);
|
||||
|
||||
meshList[] = null;
|
||||
hasMesh = false;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// List of meshes, if any. The meshes are C++ objects.
|
||||
MeshObj meshList[];
|
||||
|
||||
// Scene node. All child quads are added to this.
|
||||
SceneNode mNode;
|
||||
|
||||
// Bounding box, transformed to world coordinates. Used to calculate
|
||||
// camera distance.
|
||||
Bounds mBounds;
|
||||
|
||||
float mSplitDistance,mUnsplitDistance;
|
||||
|
||||
Quad mChildren[4];
|
||||
|
||||
// Contains the 'level' of this node. Level 1 is the closest and
|
||||
// most detailed level
|
||||
int mLevel;
|
||||
int mCellX, mCellY;
|
||||
|
||||
QuadInfo *mInfo;
|
||||
|
||||
bool hasMesh;
|
||||
bool hasChildren;
|
||||
bool isStatic; // Static quads are never split or unsplit
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/*
|
||||
OpenMW - The completely unofficial reimplementation of Morrowind
|
||||
Copyright (C) 2008-2009 Nicolay Korslund
|
||||
Email: < korslund@gmail.com >
|
||||
WWW: http://openmw.snaptoad.com/
|
||||
|
||||
This file (terrain.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 terrain.terrain;
|
||||
|
||||
import terrain.generator;
|
||||
import terrain.archive;
|
||||
import terrain.bindings;
|
||||
import terrain.quad;
|
||||
import std.file, std.stdio;
|
||||
|
||||
char[] cacheDir = "cache/terrain/";
|
||||
|
||||
// Enable this to render single terrain meshes instead of the entire
|
||||
// data set
|
||||
//debug=singleMesh;
|
||||
|
||||
void initTerrain(bool doGen)
|
||||
{
|
||||
char[] fname = cacheDir ~ "landscape.cache";
|
||||
|
||||
if(!exists(fname))
|
||||
{
|
||||
writefln("Cache file '%s' not found. Creating:",
|
||||
fname);
|
||||
doGen = true;
|
||||
}
|
||||
|
||||
if(doGen)
|
||||
generate(fname);
|
||||
|
||||
// Load the archive file
|
||||
g_archive.openFile(fname);
|
||||
|
||||
terr_setupRendering();
|
||||
|
||||
debug(singleMesh)
|
||||
{
|
||||
int X = 22;
|
||||
int Y = 0;
|
||||
bool next = false;
|
||||
|
||||
void doQuad(int x, int y, int lev, int diffx, int diffy)
|
||||
{
|
||||
if(!g_archive.hasQuad(x,y,lev))
|
||||
return;
|
||||
|
||||
diffx *= 8192;
|
||||
diffy *= 8192;
|
||||
|
||||
auto node = terr_createChildNode(20000+diffx,-60000+diffy,null);
|
||||
auto info = g_archive.getQuad(x,y,lev);
|
||||
g_archive.mapQuad(info);
|
||||
auto mi = g_archive.getMeshInfo(0);
|
||||
terr_makeMesh(node, mi, info.level, TEX_SCALE);
|
||||
}
|
||||
|
||||
doQuad(X,Y,1, 0,0);
|
||||
doQuad(X+1,Y,1, 1,0);
|
||||
doQuad(X,Y+1,1, 0,1);
|
||||
doQuad(X+1,Y+1,1, 1,1);
|
||||
|
||||
doQuad(X + (next?2:0),Y,2, 2,0);
|
||||
|
||||
doQuad(20,Y,3, 0,2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create the root quad
|
||||
rootQuad = new Quad;
|
||||
}
|
||||
}
|
||||
|
||||
extern(C) void d_terr_terrainUpdate()
|
||||
{
|
||||
debug(singleMesh) return;
|
||||
|
||||
// Update the root quad each frame.
|
||||
assert(rootQuad !is null);
|
||||
rootQuad.update();
|
||||
}
|
||||
|
||||
Quad rootQuad;
|
Loading…
Reference in New Issue