1 #include "converter.hpp" 2 3 #include <stdexcept> 4 #include <algorithm> 5 6 #include <osgDB/WriteFile> 7 8 #include <components/esm/creaturestate.hpp> 9 #include <components/esm/containerstate.hpp> 10 11 #include <components/misc/constants.hpp> 12 13 #include "convertcrec.hpp" 14 #include "convertcntc.hpp" 15 #include "convertscri.hpp" 16 17 namespace 18 { 19 convertImage(char * data,int size,int width,int height,GLenum pf,const std::string & out)20 void convertImage(char* data, int size, int width, int height, GLenum pf, const std::string& out) 21 { 22 osg::ref_ptr<osg::Image> image (new osg::Image); 23 image->allocateImage(width, height, 1, pf, GL_UNSIGNED_BYTE); 24 memcpy(image->data(), data, size); 25 image->flipVertical(); 26 27 osgDB::writeImageFile(*image, out); 28 } 29 30 convertCellRef(const ESSImport::CellRef & cellref,ESM::ObjectState & objstate)31 void convertCellRef(const ESSImport::CellRef& cellref, ESM::ObjectState& objstate) 32 { 33 objstate.mEnabled = cellref.mEnabled; 34 objstate.mPosition = cellref.mPos; 35 objstate.mRef.mRefNum = cellref.mRefNum; 36 if (cellref.mDeleted) 37 objstate.mCount = 0; 38 convertSCRI(cellref.mSCRI, objstate.mLocals); 39 objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); 40 41 if (cellref.mHasANIS) 42 convertANIS(cellref.mANIS, objstate.mAnimationState); 43 } 44 isIndexedRefId(const std::string & indexedRefId)45 bool isIndexedRefId(const std::string& indexedRefId) 46 { 47 if (indexedRefId.size() <= 8) 48 return false; 49 50 if (indexedRefId.find_first_not_of("0123456789") == std::string::npos) 51 return false; // entirely numeric refid, this is a reference to 52 // a dynamically created record e.g. player-enchanted weapon 53 54 std::string index = indexedRefId.substr(indexedRefId.size()-8); 55 return index.find_first_not_of("0123456789ABCDEF") == std::string::npos; 56 } 57 splitIndexedRefId(const std::string & indexedRefId,int & refIndex,std::string & refId)58 void splitIndexedRefId(const std::string& indexedRefId, int& refIndex, std::string& refId) 59 { 60 std::stringstream stream; 61 stream << std::hex << indexedRefId.substr(indexedRefId.size()-8,8); 62 stream >> refIndex; 63 64 refId = indexedRefId.substr(0,indexedRefId.size()-8); 65 } 66 convertActorId(const std::string & indexedRefId,ESSImport::Context & context)67 int convertActorId(const std::string& indexedRefId, ESSImport::Context& context) 68 { 69 if (isIndexedRefId(indexedRefId)) 70 { 71 int refIndex; 72 std::string refId; 73 splitIndexedRefId(indexedRefId, refIndex, refId); 74 75 auto it = context.mActorIdMap.find(std::make_pair(refIndex, refId)); 76 if (it == context.mActorIdMap.end()) 77 return -1; 78 return it->second; 79 } 80 else if (indexedRefId == "PlayerSaveGame") 81 { 82 return context.mPlayer.mObject.mCreatureStats.mActorId; 83 } 84 85 return -1; 86 } 87 } 88 89 namespace ESSImport 90 { 91 92 93 struct MAPH 94 { 95 unsigned int size; 96 unsigned int value; 97 }; 98 read(ESM::ESMReader & esm)99 void ConvertFMAP::read(ESM::ESMReader &esm) 100 { 101 MAPH maph; 102 esm.getHNT(maph, "MAPH"); 103 std::vector<char> data; 104 esm.getSubNameIs("MAPD"); 105 esm.getSubHeader(); 106 data.resize(esm.getSubSize()); 107 esm.getExact(&data[0], data.size()); 108 109 mGlobalMapImage = new osg::Image; 110 mGlobalMapImage->allocateImage(maph.size, maph.size, 1, GL_RGB, GL_UNSIGNED_BYTE); 111 memcpy(mGlobalMapImage->data(), &data[0], data.size()); 112 113 // to match openmw size 114 // FIXME: filtering? 115 mGlobalMapImage->scaleImage(maph.size*2, maph.size*2, 1, GL_UNSIGNED_BYTE); 116 } 117 write(ESM::ESMWriter & esm)118 void ConvertFMAP::write(ESM::ESMWriter &esm) 119 { 120 int numcells = mGlobalMapImage->s() / 18; // NB truncating, doesn't divide perfectly 121 // with the 512x512 map the game has by default 122 int cellSize = mGlobalMapImage->s()/numcells; 123 124 // Note the upper left corner of the (0,0) cell should be at (width/2, height/2) 125 126 mContext->mGlobalMapState.mBounds.mMinX = -numcells/2; 127 mContext->mGlobalMapState.mBounds.mMaxX = (numcells-1)/2; 128 mContext->mGlobalMapState.mBounds.mMinY = -(numcells-1)/2; 129 mContext->mGlobalMapState.mBounds.mMaxY = numcells/2; 130 131 osg::ref_ptr<osg::Image> image2 (new osg::Image); 132 int width = cellSize*numcells; 133 int height = cellSize*numcells; 134 std::vector<unsigned char> data; 135 data.resize(width*height*4, 0); 136 137 image2->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); 138 memcpy(image2->data(), &data[0], data.size()); 139 140 for (const auto & exploredCell : mContext->mExploredCells) 141 { 142 if (exploredCell.first > mContext->mGlobalMapState.mBounds.mMaxX 143 || exploredCell.first < mContext->mGlobalMapState.mBounds.mMinX 144 || exploredCell.second > mContext->mGlobalMapState.mBounds.mMaxY 145 || exploredCell.second < mContext->mGlobalMapState.mBounds.mMinY) 146 { 147 // out of bounds, I think this could happen, since the original engine had a fixed-size map 148 continue; 149 } 150 151 int imageLeftSrc = mGlobalMapImage->s()/2; 152 int imageTopSrc = mGlobalMapImage->t()/2; 153 imageLeftSrc += exploredCell.first * cellSize; 154 imageTopSrc -= exploredCell.second * cellSize; 155 int imageLeftDst = width/2; 156 int imageTopDst = height/2; 157 imageLeftDst += exploredCell.first * cellSize; 158 imageTopDst -= exploredCell.second * cellSize; 159 for (int x=0; x<cellSize; ++x) 160 for (int y=0; y<cellSize; ++y) 161 { 162 unsigned int col = *(unsigned int*)mGlobalMapImage->data(imageLeftSrc+x, imageTopSrc+y, 0); 163 *(unsigned int*)image2->data(imageLeftDst+x, imageTopDst+y, 0) = col; 164 } 165 } 166 167 std::stringstream ostream; 168 osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); 169 if (!readerwriter) 170 { 171 std::cerr << "Error: can't write global map image, no png readerwriter found" << std::endl; 172 return; 173 } 174 175 image2->flipVertical(); 176 177 osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image2, ostream); 178 if (!result.success()) 179 { 180 std::cerr << "Error: can't write global map image: " << result.message() << " code " << result.status() << std::endl; 181 return; 182 } 183 184 std::string outData = ostream.str(); 185 mContext->mGlobalMapState.mImageData = std::vector<char>(outData.begin(), outData.end()); 186 187 esm.startRecord(ESM::REC_GMAP); 188 mContext->mGlobalMapState.save(esm); 189 esm.endRecord(ESM::REC_GMAP); 190 } 191 read(ESM::ESMReader & esm)192 void ConvertCell::read(ESM::ESMReader &esm) 193 { 194 ESM::Cell cell; 195 bool isDeleted = false; 196 197 cell.load(esm, isDeleted, false); 198 199 // I wonder what 0x40 does? 200 if (cell.isExterior() && cell.mData.mFlags & 0x20) 201 { 202 mContext->mGlobalMapState.mMarkers.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); 203 } 204 205 // note if the player is in a nameless exterior cell, we will assign the cellId later based on player position 206 if (cell.mName == mContext->mPlayerCellName) 207 { 208 mContext->mPlayer.mCellId = cell.getCellId(); 209 } 210 211 Cell newcell; 212 newcell.mCell = cell; 213 214 // fog of war 215 // seems to be a 1-bit pixel format, 16*16 pixels 216 // TODO: add bleeding of FOW into neighbouring cells (openmw handles this by writing to the textures, 217 // MW handles it when rendering only) 218 unsigned char nam8[32]; 219 // exterior has 1 NAM8, interior can have multiple ones, and have an extra 4 byte flag at the start 220 // (probably offset of that specific fog texture?) 221 while (esm.isNextSub("NAM8")) 222 { 223 if (cell.isExterior()) // TODO: NAM8 occasionally exists for cells that haven't been explored. 224 // are there any flags marking explored cells? 225 mContext->mExploredCells.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); 226 227 esm.getSubHeader(); 228 229 if (esm.getSubSize() == 36) 230 { 231 // flag on interiors 232 esm.skip(4); 233 } 234 235 esm.getExact(nam8, 32); 236 237 newcell.mFogOfWar.reserve(16*16); 238 for (int x=0; x<16; ++x) 239 { 240 for (int y=0; y<16; ++y) 241 { 242 size_t pos = x*16+y; 243 size_t bytepos = pos/8; 244 assert(bytepos<32); 245 int bit = pos%8; 246 newcell.mFogOfWar.push_back(((nam8[bytepos] >> bit) & (0x1)) ? 0xffffffff : 0x000000ff); 247 } 248 } 249 250 if (cell.isExterior()) 251 { 252 std::ostringstream filename; 253 filename << "fog_" << cell.mData.mX << "_" << cell.mData.mY << ".tga"; 254 255 convertImage((char*)&newcell.mFogOfWar[0], newcell.mFogOfWar.size()*4, 16, 16, GL_RGBA, filename.str()); 256 } 257 } 258 259 // moved reference, not handled yet 260 // NOTE: MVRF can also occur in within normal references (importcellref.cpp)? 261 // this does not match the ESM file implementation, 262 // verify if that can happen with ESM files too 263 while (esm.isNextSub("MVRF")) 264 { 265 esm.skipHSub(); // skip MVRF 266 esm.getSubName(); 267 esm.skipHSub(); // skip CNDT 268 } 269 270 std::vector<CellRef> cellrefs; 271 while (esm.hasMoreSubs() && esm.isNextSub("FRMR")) 272 { 273 CellRef ref; 274 ref.load (esm); 275 cellrefs.push_back(ref); 276 } 277 278 while (esm.isNextSub("MPCD")) 279 { 280 float notepos[3]; 281 esm.getHT(notepos, 3*sizeof(float)); 282 283 // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. 284 // This seems to be the reason markers can't be placed everywhere in interior cells, 285 // i.e. when the grid is exceeded. 286 // Converting the interior markers correctly could be rather tricky, but is probably similar logic 287 // as used for the FoW texture placement, which we need to figure out anyway 288 notepos[1] += 31.f; 289 notepos[0] += 0.5; 290 notepos[1] += 0.5; 291 notepos[0] = Constants::CellSizeInUnits * notepos[0] / 32.f; 292 notepos[1] = Constants::CellSizeInUnits * notepos[1] / 32.f; 293 if (cell.isExterior()) 294 { 295 notepos[0] += Constants::CellSizeInUnits * cell.mData.mX; 296 notepos[1] += Constants::CellSizeInUnits * cell.mData.mY; 297 } 298 // TODO: what encoding is this in? 299 std::string note = esm.getHNString("MPNT"); 300 ESM::CustomMarker marker; 301 marker.mWorldX = notepos[0]; 302 marker.mWorldY = notepos[1]; 303 marker.mNote = note; 304 marker.mCell = cell.getCellId(); 305 mMarkers.push_back(marker); 306 } 307 308 newcell.mRefs = cellrefs; 309 310 311 if (cell.isExterior()) 312 mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; 313 else 314 mIntCells[cell.mName] = newcell; 315 } 316 writeCell(const Cell & cell,ESM::ESMWriter & esm)317 void ConvertCell::writeCell(const Cell &cell, ESM::ESMWriter& esm) 318 { 319 ESM::Cell esmcell = cell.mCell; 320 esm.startRecord(ESM::REC_CSTA); 321 ESM::CellState csta; 322 csta.mHasFogOfWar = 0; 323 csta.mId = esmcell.getCellId(); 324 csta.mId.save(esm); 325 // TODO csta.mLastRespawn; 326 // shouldn't be needed if we respawn on global schedule like in original MW 327 csta.mWaterLevel = esmcell.mWater; 328 csta.save(esm); 329 330 for (const auto & cellref : cell.mRefs) 331 { 332 ESM::CellRef out (cellref); 333 334 // TODO: use mContext->mCreatures/mNpcs 335 336 if (!isIndexedRefId(cellref.mIndexedRefId)) 337 { 338 // non-indexed RefNum, i.e. no CREC/NPCC/CNTC record associated with it 339 // this could be any type of object really (even creatures/npcs too) 340 out.mRefID = cellref.mIndexedRefId; 341 std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); 342 343 ESM::ObjectState objstate; 344 objstate.blank(); 345 objstate.mRef = out; 346 objstate.mRef.mRefID = idLower; 347 objstate.mHasCustomState = false; 348 convertCellRef(cellref, objstate); 349 esm.writeHNT ("OBJE", 0); 350 objstate.save(esm); 351 continue; 352 } 353 else 354 { 355 int refIndex; 356 splitIndexedRefId(cellref.mIndexedRefId, refIndex, out.mRefID); 357 358 std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); 359 360 std::map<std::pair<int, std::string>, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( 361 std::make_pair(refIndex, out.mRefID)); 362 if (npccIt != mContext->mNpcChanges.end()) 363 { 364 ESM::NpcState objstate; 365 objstate.blank(); 366 objstate.mRef = out; 367 objstate.mRef.mRefID = idLower; 368 // TODO: need more micromanagement here so we don't overwrite values 369 // from the ESM with default values 370 if (cellref.mHasACDT) 371 convertACDT(cellref.mACDT, objstate.mCreatureStats); 372 if (cellref.mHasACSC) 373 convertACSC(cellref.mACSC, objstate.mCreatureStats); 374 convertNpcData(cellref, objstate.mNpcStats); 375 convertNPCC(npccIt->second, objstate); 376 convertCellRef(cellref, objstate); 377 378 objstate.mCreatureStats.mActorId = mContext->generateActorId(); 379 mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); 380 381 esm.writeHNT ("OBJE", ESM::REC_NPC_); 382 objstate.save(esm); 383 continue; 384 } 385 386 std::map<std::pair<int, std::string>, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( 387 std::make_pair(refIndex, out.mRefID)); 388 if (cntcIt != mContext->mContainerChanges.end()) 389 { 390 ESM::ContainerState objstate; 391 objstate.blank(); 392 objstate.mRef = out; 393 objstate.mRef.mRefID = idLower; 394 convertCNTC(cntcIt->second, objstate); 395 convertCellRef(cellref, objstate); 396 esm.writeHNT ("OBJE", ESM::REC_CONT); 397 objstate.save(esm); 398 continue; 399 } 400 401 std::map<std::pair<int, std::string>, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( 402 std::make_pair(refIndex, out.mRefID)); 403 if (crecIt != mContext->mCreatureChanges.end()) 404 { 405 ESM::CreatureState objstate; 406 objstate.blank(); 407 objstate.mRef = out; 408 objstate.mRef.mRefID = idLower; 409 // TODO: need more micromanagement here so we don't overwrite values 410 // from the ESM with default values 411 if (cellref.mHasACDT) 412 convertACDT(cellref.mACDT, objstate.mCreatureStats); 413 if (cellref.mHasACSC) 414 convertACSC(cellref.mACSC, objstate.mCreatureStats); 415 convertCREC(crecIt->second, objstate); 416 convertCellRef(cellref, objstate); 417 418 objstate.mCreatureStats.mActorId = mContext->generateActorId(); 419 mContext->mActorIdMap.insert(std::make_pair(std::make_pair(refIndex, out.mRefID), objstate.mCreatureStats.mActorId)); 420 421 esm.writeHNT ("OBJE", ESM::REC_CREA); 422 objstate.save(esm); 423 continue; 424 } 425 426 std::stringstream error; 427 error << "Can't find type for " << cellref.mIndexedRefId << std::endl; 428 throw std::runtime_error(error.str()); 429 } 430 } 431 432 esm.endRecord(ESM::REC_CSTA); 433 } 434 write(ESM::ESMWriter & esm)435 void ConvertCell::write(ESM::ESMWriter &esm) 436 { 437 for (const auto & cell : mIntCells) 438 writeCell(cell.second, esm); 439 440 for (const auto & cell : mExtCells) 441 writeCell(cell.second, esm); 442 443 for (const auto & marker : mMarkers) 444 { 445 esm.startRecord(ESM::REC_MARK); 446 marker.save(esm); 447 esm.endRecord(ESM::REC_MARK); 448 } 449 } 450 read(ESM::ESMReader & esm)451 void ConvertPROJ::read(ESM::ESMReader& esm) 452 { 453 mProj.load(esm); 454 } 455 write(ESM::ESMWriter & esm)456 void ConvertPROJ::write(ESM::ESMWriter& esm) 457 { 458 for (const PROJ::PNAM& pnam : mProj.mProjectiles) 459 { 460 if (!pnam.isMagic()) 461 { 462 ESM::ProjectileState out; 463 convertBaseState(out, pnam); 464 465 out.mBowId = pnam.mBowId.toString(); 466 out.mVelocity = pnam.mVelocity; 467 out.mAttackStrength = pnam.mAttackStrength; 468 469 esm.startRecord(ESM::REC_PROJ); 470 out.save(esm); 471 esm.endRecord(ESM::REC_PROJ); 472 } 473 else 474 { 475 ESM::MagicBoltState out; 476 convertBaseState(out, pnam); 477 478 auto it = std::find_if(mContext->mActiveSpells.begin(), mContext->mActiveSpells.end(), 479 [&pnam](const SPLM::ActiveSpell& spell) -> bool { return spell.mIndex == pnam.mSplmIndex; }); 480 481 if (it == mContext->mActiveSpells.end()) 482 { 483 std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (invalid spell link)" << std::endl; 484 continue; 485 } 486 487 out.mSpellId = it->mSPDT.mId.toString(); 488 out.mSpeed = pnam.mSpeed * 0.001f; // not sure where this factor comes from 489 490 esm.startRecord(ESM::REC_MPRJ); 491 out.save(esm); 492 esm.endRecord(ESM::REC_MPRJ); 493 } 494 } 495 } 496 convertBaseState(ESM::BaseProjectileState & base,const PROJ::PNAM & pnam)497 void ConvertPROJ::convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam) 498 { 499 base.mId = pnam.mArrowId.toString(); 500 base.mPosition = pnam.mPosition; 501 502 osg::Quat orient; 503 orient.makeRotate(osg::Vec3f(0,1,0), pnam.mVelocity); 504 base.mOrientation = orient; 505 506 base.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); 507 } 508 read(ESM::ESMReader & esm)509 void ConvertSPLM::read(ESM::ESMReader& esm) 510 { 511 mSPLM.load(esm); 512 mContext->mActiveSpells = mSPLM.mActiveSpells; 513 } 514 write(ESM::ESMWriter & esm)515 void ConvertSPLM::write(ESM::ESMWriter& esm) 516 { 517 std::cerr << "Warning: Skipped active spell conversion (not implemented)" << std::endl; 518 } 519 520 } 521