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