Merging in master
commit
38a0d1d1c5
@ -0,0 +1,193 @@
|
||||
#include "journalwindow.hpp"
|
||||
#include "window_manager.hpp"
|
||||
#include "../mwdialogue/journal.hpp"
|
||||
#include "../mwworld/environment.hpp"
|
||||
#include "../mwworld/world.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct book
|
||||
{
|
||||
int endLine;
|
||||
std::list<std::string> pages;
|
||||
};
|
||||
}
|
||||
|
||||
book formatText(std::string text,book mBook,int maxLine, int lineSize)
|
||||
{
|
||||
//stringList.push_back("");
|
||||
|
||||
int cLineSize = 0;
|
||||
int cLine = mBook.endLine +1;
|
||||
std::string cString;
|
||||
|
||||
if(mBook.pages.empty())
|
||||
{
|
||||
cString = "";
|
||||
cLine = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
cString = mBook.pages.back() + std::string("\n");
|
||||
mBook.pages.pop_back();
|
||||
}
|
||||
|
||||
//std::string::iterator wordBegin = text.begin();
|
||||
//std::string::iterator wordEnd;
|
||||
|
||||
std::string cText = text;
|
||||
|
||||
while(cText.length() != 0)
|
||||
{
|
||||
size_t firstSpace = cText.find_first_of(' ');
|
||||
if(firstSpace == std::string::npos)
|
||||
{
|
||||
cString = cString + cText;
|
||||
mBook.pages.push_back(cString);
|
||||
//TODO:finnish this
|
||||
break;
|
||||
}
|
||||
if(static_cast<int> (firstSpace) + cLineSize <= lineSize)
|
||||
{
|
||||
cLineSize = firstSpace + cLineSize;
|
||||
cString = cString + cText.substr(0,firstSpace +1);
|
||||
}
|
||||
else
|
||||
{
|
||||
cLineSize = firstSpace;
|
||||
if(cLine +1 <= maxLine)
|
||||
{
|
||||
cLine = cLine + 1;
|
||||
cString = cString + std::string("\n") + cText.substr(0,firstSpace +1);
|
||||
}
|
||||
else
|
||||
{
|
||||
cLine = 0;
|
||||
mBook.pages.push_back(cString);
|
||||
cString = cText.substr(0,firstSpace +1);
|
||||
}
|
||||
}
|
||||
//std::cout << cText << "\n";
|
||||
//std::cout << cText.length();
|
||||
cText = cText.substr(firstSpace +1,cText.length() - firstSpace -1);
|
||||
}
|
||||
mBook.endLine = cLine;
|
||||
return mBook;
|
||||
//std::string
|
||||
}
|
||||
|
||||
|
||||
MWGui::JournalWindow::JournalWindow (WindowManager& parWindowManager)
|
||||
: WindowBase("openmw_journal_layout.xml", parWindowManager)
|
||||
, lastPos(0)
|
||||
{
|
||||
//setCoord(0,0,498, 342);
|
||||
center();
|
||||
|
||||
getWidget(mLeftTextWidget, "LeftText");
|
||||
getWidget(mRightTextWidget, "RightText");
|
||||
getWidget(mPrevBtn, "PrevPageBTN");
|
||||
mPrevBtn->eventMouseButtonClick = MyGUI::newDelegate(this,&MWGui::JournalWindow::notifyPrevPage);
|
||||
getWidget(mNextBtn, "NextPageBTN");
|
||||
mNextBtn->eventMouseButtonClick = MyGUI::newDelegate(this,&MWGui::JournalWindow::notifyNextPage);
|
||||
//MyGUI::ItemBox* list = new MyGUI::ItemBox();
|
||||
//list->addItem("qaq","aqzazaz");
|
||||
//mScrollerWidget->addChildItem(list);
|
||||
//mScrollerWidget->addItem("dserzt",MyGUI::UString("fedgdfg"));
|
||||
//mEditWidget->addText("ljblsxdvdsfvgedfvdfvdkjfghldfjgn sdv,nhsxl;vvn lklksbvlksb lbsdflkbdSLKJGBLskdhbvl<kbvlqksbgkqsjhdvb");
|
||||
//mEditWidget->show();
|
||||
//mEditWidget->setEditStatic(true);
|
||||
mLeftTextWidget->addText("left texxxt ");
|
||||
mLeftTextWidget->setEditReadOnly(true);
|
||||
mRightTextWidget->setEditReadOnly(true);
|
||||
mRightTextWidget->setEditStatic(true);
|
||||
mLeftTextWidget->setEditStatic(true);
|
||||
mRightTextWidget->addText("Right texxt ");
|
||||
|
||||
//std::list<std::string> list = formatText("OpenMW rgh dsfg sqef srg ZT uzql n ZLIEHRF LQSJH GLOIjf qjfmj hslkdgn jlkdjhg qlr isgli shli uhs fiuh qksf cg ksjnf lkqsnbf ksbf sbfkl zbf kuyzflkj sbgdfkj zlfh ozhjfmo hzmfh lizuf rty qzt ezy tkyEZT RYYJ DG fgh is an open-source implementation of the game engine found in the game Morrowind. This is a dumb test text msodjbg smojg smoig fiiinnn");
|
||||
//std::list<std::string> list = formatText();
|
||||
//displayLeftText(list.front());
|
||||
|
||||
MyGUI::WindowPtr t = static_cast<MyGUI::WindowPtr>(mMainWidget);
|
||||
t->eventWindowChangeCoord = MyGUI::newDelegate(this, &JournalWindow::onWindowResize);
|
||||
}
|
||||
|
||||
void MWGui::JournalWindow::open()
|
||||
{
|
||||
mPageNumber = 0;
|
||||
if(mWindowManager.getEnvironment().mJournal->begin()!=mWindowManager.getEnvironment().mJournal->end())
|
||||
{
|
||||
book journal;
|
||||
journal.endLine = 0;
|
||||
|
||||
for(std::deque<MWDialogue::StampedJournalEntry>::const_iterator it = mWindowManager.getEnvironment().mJournal->begin();it!=mWindowManager.getEnvironment().mJournal->end();it++)
|
||||
{
|
||||
std::string a = it->getText(mWindowManager.getEnvironment().mWorld->getStore());
|
||||
journal = formatText(a,journal,10,17);
|
||||
journal.endLine = journal.endLine +1;
|
||||
journal.pages.back() = journal.pages.back() + std::string("\n");
|
||||
}
|
||||
//std::string a = mWindowManager.getEnvironment().mJournal->begin()->getText(mWindowManager.getEnvironment().mWorld->getStore());
|
||||
//std::list<std::string> journal = formatText(a,10,20,1);
|
||||
bool left = true;
|
||||
for(std::list<std::string>::iterator it = journal.pages.begin(); it != journal.pages.end();it++)
|
||||
{
|
||||
if(left)
|
||||
{
|
||||
leftPages.push_back(*it);
|
||||
}
|
||||
else
|
||||
{
|
||||
rightPages.push_back(*it);
|
||||
}
|
||||
left = !left;
|
||||
}
|
||||
if(!left) rightPages.push_back("");
|
||||
|
||||
mPageNumber = leftPages.size()-1;
|
||||
displayLeftText(leftPages[mPageNumber]);
|
||||
displayRightText(rightPages[mPageNumber]);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//std::cout << mWindowManager.getEnvironment().mJournal->begin()->getText(mWindowManager.getEnvironment().mWorld->getStore());
|
||||
}
|
||||
}
|
||||
|
||||
void MWGui::JournalWindow::onWindowResize(MyGUI::Window* window)
|
||||
{
|
||||
}
|
||||
|
||||
void MWGui::JournalWindow::displayLeftText(std::string text)
|
||||
{
|
||||
mLeftTextWidget->eraseText(0,mLeftTextWidget->getTextLength());
|
||||
mLeftTextWidget->addText(text);
|
||||
}
|
||||
|
||||
void MWGui::JournalWindow::displayRightText(std::string text)
|
||||
{
|
||||
mRightTextWidget->eraseText(0,mRightTextWidget->getTextLength());
|
||||
mRightTextWidget->addText(text);
|
||||
}
|
||||
|
||||
|
||||
void MWGui::JournalWindow::notifyNextPage(MyGUI::WidgetPtr _sender)
|
||||
{
|
||||
if(mPageNumber < int(leftPages.size())-1)
|
||||
{
|
||||
mPageNumber = mPageNumber + 1;
|
||||
displayLeftText(leftPages[mPageNumber]);
|
||||
displayRightText(rightPages[mPageNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
void MWGui::JournalWindow::notifyPrevPage(MyGUI::WidgetPtr _sender)
|
||||
{
|
||||
if(mPageNumber > 0)
|
||||
{
|
||||
mPageNumber = mPageNumber - 1;
|
||||
displayLeftText(leftPages[mPageNumber]);
|
||||
displayRightText(rightPages[mPageNumber]);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
#ifndef MWGUI_JOURNAL_H
|
||||
#define MWGUI_JOURNAL_H
|
||||
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "window_base.hpp"
|
||||
|
||||
namespace MWGui
|
||||
{
|
||||
class WindowManager;
|
||||
|
||||
class JournalWindow : public WindowBase
|
||||
{
|
||||
public:
|
||||
JournalWindow(WindowManager& parWindowManager);
|
||||
void open();
|
||||
|
||||
private:
|
||||
enum ColorStyle
|
||||
{
|
||||
CS_Sub,
|
||||
CS_Normal,
|
||||
CS_Super
|
||||
};
|
||||
|
||||
void onWindowResize(MyGUI::Window* window);
|
||||
|
||||
void displayLeftText(std::string text);
|
||||
void displayRightText(std::string text);
|
||||
|
||||
|
||||
/**
|
||||
*Called when next/prev button is used.
|
||||
*/
|
||||
void notifyNextPage(MyGUI::WidgetPtr _sender);
|
||||
void notifyPrevPage(MyGUI::WidgetPtr _sender);
|
||||
|
||||
static const int lineHeight;
|
||||
|
||||
MyGUI::WidgetPtr skillAreaWidget, skillClientWidget;
|
||||
MyGUI::VScrollPtr skillScrollerWidget;
|
||||
int lastPos, clientHeight;
|
||||
MyGUI::EditPtr mLeftTextWidget;
|
||||
MyGUI::EditPtr mRightTextWidget;
|
||||
MyGUI::ButtonPtr mPrevBtn;
|
||||
MyGUI::ButtonPtr mNextBtn;
|
||||
std::vector<std::string> leftPages;
|
||||
std::vector<std::string> rightPages;
|
||||
int mPageNumber; //store the number of the current left page
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<MyGUI type="Skin">
|
||||
<Skin name="Next_btn" size="64 32" texture="textures\tx_menubook_next_idle.dds">
|
||||
<BasisSkin type="MainSkin" offset="0 0 64 32" align="ALIGN_VSTRETCH">
|
||||
<State name="normal" offset="0 0 64 32"/>
|
||||
</BasisSkin>
|
||||
</Skin>
|
||||
|
||||
<Skin name="Prev_btn" size="128 32" texture="textures\tx_menubook_prev_idle.dds">
|
||||
<BasisSkin type="MainSkin" offset="0 0 128 32" align="ALIGN_VSTRETCH">
|
||||
<State name="normal" offset="0 0 128 32"/>
|
||||
</BasisSkin>
|
||||
</Skin>
|
||||
|
||||
<Skin name = "MW_BookClient" size = "10 10">
|
||||
<Property key="FontName" value = "MonoFont" />
|
||||
<Property key="AlignText" value = "Left Top" />
|
||||
<Property key="Colour" value = "0000FF" />
|
||||
<!--Property key="Pointer" value = "beam" /-->
|
||||
<BasisSkin type="EditText" offset = "0 0 10 10" align = "Stretch"/>
|
||||
</Skin>
|
||||
|
||||
<Skin name="MW_BookPage" size="0 0 50 50">
|
||||
<Property key="WordWrap" value = "true" />
|
||||
<Child type="Widget" skin="MW_BookClient" offset="0 0 35 10" align = "ALIGN_STRETCH" name = "Client"/>
|
||||
<!--Child type="VScroll" skin="VScroll" offset = "35 0 15 50" align = "Right VStretch" name = "VScroll"/-->
|
||||
</Skin>
|
||||
</MyGUI>
|
@ -1 +1 @@
|
||||
Subproject commit 2f5eca9d878526bdd9dce93ece7f42093b481545
|
||||
Subproject commit 4a188f2820c037ca4ad8ef35492d3857ea8d7df8
|
@ -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