Files
minecraft-pe-0.6.1/src/world/level/storage/ExternalFileLevelStorage.cpp
2026-03-27 21:26:25 +03:00

703 lines
20 KiB
C++
Executable File

#include <cstddef>
#include <fstream>
#include <ios>
#if !defined(DEMO_MODE) && !defined(APPLE_DEMO_PROMOTION)
#include "LevelData.h"
#include "RegionFile.h"
#include "ExternalFileLevelStorage.h"
#include "FolderMethods.h"
#include "../chunk/LevelChunk.h"
#include "../Level.h"
#include "../LevelConstants.h"
#include "platform/log.h"
#include "../tile/TreeTile.h"
#include "../../entity/EntityFactory.h"
#include "../../../nbt/NbtIo.h"
#include "../../../util/RakDataIO.h"
#include "../../../raknet/GetTime.h"
#include "../tile/entity/TileEntity.h"
static const int ChunkVersion_Light = 1;
static const int ChunkVersion_Entity = 2;
const char* const fnLevelDatOld = "level.dat_old";
const char* const fnLevelDatNew = "level.dat_new";
const char* const fnLevelDat = "level.dat";
const char* const fnPlayerDat = "player.dat";
//
// Helpers for converting old levels to newer
//
class LevelConverters
{
public:
// Replacing old Cloth (id based) with new Cloth (data based)
static bool v1_ClothIdToClothData(LevelChunk* c) {
bool changed = false;
unsigned char* blocks = c->getBlockData();
unsigned char newTile = Tile::cloth->id;
for (int i = 0; i < 16*16*128; ++i) {
unsigned char oldTile = blocks[i];
//Tile::cloth_00 to Tile::cloth_61
if (oldTile >= 101 && oldTile <= 115) {
int color = 0xf - (oldTile - 101);
blocks[i] = newTile;
c->data.set(i, color);
changed = true;
}
}
return changed;
}
// Replacing unavailable blocks with "Update!" blocks
static bool ReplaceUnavailableBlocks(LevelChunk* c) {
//int st = getTimeMs();
bool changed = false;
unsigned char* blocks = c->getBlockData();
for (int i = 0; i < 16*16*128; ++i) {
unsigned char oldTile = blocks[i];
unsigned char newTile = Tile::transformToValidBlockId(oldTile);
if (oldTile != newTile) {
blocks[i] = newTile;
changed = true;
}
}
//int et = getTimeMs();
//LOGI("time: %d\n", et - st);
return changed;
}
//static bool ConvertPlayerDatToLevelDat() {
// return false;
//}
};
ExternalFileLevelStorage::ExternalFileLevelStorage(const std::string& levelId, const std::string& fullPath)
: levelId(levelId),
levelPath(fullPath),
loadedLevelData(NULL),
regionFile(NULL),
entitiesFile(NULL),
tickCount(0),
lastSavedEntitiesTick(-999999),
level(NULL),
loadedStorageVersion(SharedConstants::StorageVersion)
{
createFolderIfNotExists(levelPath.c_str());
std::string playerFolder = levelPath + "/players";
createFolderIfNotExists(playerFolder.c_str());
std::string datFileName = levelPath + "/" + fnLevelDat;
std::string levelFileName = levelPath + "/" + fnPlayerDat;
loadedLevelData = new LevelData();
if (readLevelData(levelPath, *loadedLevelData))
{
loadedStorageVersion = loadedLevelData->getStorageVersion();
readPlayerData(levelFileName, *loadedLevelData);
} else {
delete loadedLevelData;
loadedLevelData = NULL;
}
}
ExternalFileLevelStorage::~ExternalFileLevelStorage()
{
delete regionFile;
delete loadedLevelData;
}
void ExternalFileLevelStorage::saveLevelData(LevelData& levelData, std::vector<Player*>* players) {
ExternalFileLevelStorage::saveLevelData(levelPath, levelData, players);
}
void ExternalFileLevelStorage::saveLevelData( const std::string& levelPath, LevelData& levelData, std::vector<Player*>* players )
{
// @todo: completely rewrite
std::string directory = levelPath + "/";
std::string tmpFile = directory + fnLevelDatNew;
std::string datFile = directory + fnLevelDat;
std::string oldFile = directory + fnLevelDatOld;
levelData.setStorageVersion(SharedConstants::StorageVersion);
if (!writeLevelData(tmpFile, levelData, players))
return;
// Remove old backup
remove(oldFile.c_str());
// If it exists, move the previous save to backup (and possibly delete it)
if (exists(datFile.c_str())) {
if (rename(datFile.c_str(), oldFile.c_str())) {
LOGE("Error@saveLevelData: Couldn't move savefile to level.dat_old\n");
return;
}
remove(datFile.c_str());
}
// Move the new save to level.dat
if (rename(tmpFile.c_str(), datFile.c_str())) {
LOGE("Error@saveLevelData: Couldn't move new file to level.dat\n");
return;
}
// Remove the temporary save, if the rename didn't do it
remove(tmpFile.c_str());
// Save players
// fuck mojang for that
if (!players || players->empty()) {
return;
}
for (auto& player : *players) {
if (player != NULL) {
savePlayer(*player, directory);
}
}
}
void ExternalFileLevelStorage::savePlayer(Player& player, const std::string& worldDir) {
std::string playerPath = worldDir + "/players/" + player.name + ".dat";
LOGI("Saving player %s to %s...\n", player.name.c_str(), playerPath.c_str());
RakNet::BitStream data;
RakDataOutput buf(data);
CompoundTag playerTag;
player.saveWithoutId(&playerTag);
NbtIo::write(&playerTag, &buf);
std::ofstream file(playerPath, std::ios::out | std::ios::binary);
file.write((const char*)data.GetData(), (size_t)data.GetNumberOfBytesUsed());
}
bool ExternalFileLevelStorage::loadPlayer(Player& player, const std::string& worldDir) {
std::string playerPath = worldDir + "/players/" + player.name + ".dat";
LOGI("Loading player %s from %s...\n", player.name.c_str(), playerPath.c_str());
std::ifstream file(playerPath, std::ios::in | std::ios::binary);
if (!file.is_open()) {
return false;
}
std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
RakNet::BitStream bitStream(data.data(), data.size(), false);
RakDataInput stream(bitStream);
CompoundTag* tag = NbtIo::read(&stream);
if (tag) {
player.load(tag);
tag->deleteChildren();
delete tag;
}
return true;
}
void ExternalFileLevelStorage::savePlayer(Player& player) {
ExternalFileLevelStorage::savePlayer(player, levelPath);
}
bool ExternalFileLevelStorage::loadPlayer(Player& player) {
return ExternalFileLevelStorage::loadPlayer(player, levelPath);
}
LevelData* ExternalFileLevelStorage::prepareLevel(Level* _level)
{
level = _level;
return loadedLevelData;
}
bool ExternalFileLevelStorage::readLevelData(const std::string& directory, LevelData& levelData)
{
// Try to load level.dat
std::string datFilename = directory + "/" + fnLevelDat;
FILE* file = fopen(datFilename.c_str(), "rb");
// If that fails, try to load level.dat_old
if (!file) {
datFilename = directory + "/" + fnLevelDatOld;
file = fopen(datFilename.c_str(), "rb");
}
if (!file)
return false;
int version = 0;
int size = 0;
unsigned char* data = NULL;
do {
if (fread(&version, sizeof(version), 1, file) != 1)
{
break;
}
if (fread(&size, sizeof(size), 1, file) != 1)
{
break;
}
int left = getRemainingFileSize(file);
if (size > left || size <= 0)
break;
data = new unsigned char[size];
if (fread(data, 1, size, file) != size)
{
break;
}
if (version == 1) {
RakNet::BitStream bitStream(data, size, false);
levelData.v1_read(bitStream, version);
} else if (version >= 2) {
//LOGI("---> Trying to load level with version %d\n", version);
RakNet::BitStream bitStream(data, size, false);
RakDataInput stream(bitStream);
//LOGI("dat: %s\n", datFileName.c_str());
CompoundTag* tag = NbtIo::read(&stream);
if (tag) {
levelData.getTagData(tag);
tag->deleteChildren();
delete tag;
}
//LOGI("<--- Finished reading level tag: %p\n", tag);
}
} while (false);
fclose(file);
delete [] data;
return true;
}
bool ExternalFileLevelStorage::writeLevelData(const std::string& datFileName, LevelData& levelData, const std::vector<Player*>* players)
{
LOGI("Writing down level seed as: %ld\n", levelData.getSeed());
//return true;
// Write level info
FILE* file = fopen(datFileName.c_str(), "wb");
if (!file)
return false;
//if (levelData.getStorageVersion() == 1) {
RakNet::BitStream data;
if (levelData.getStorageVersion() == 1)
levelData.v1_write(data);
else {
RakDataOutput buf(data);
//LOGI("---> Trying to write level with version %d\n", version);
CompoundTag* tag = NULL;
if (players && !players->empty())
tag = levelData.createTag(*players);
else
tag = levelData.createTag();
NbtIo::write(tag, &buf);
tag->deleteChildren();
delete tag;
//LOGI("<--- Finished writing level data. Size: %d\n", fdout.bytesWritten);
}
int version = levelData.getStorageVersion(); // 1
fwrite(&version, sizeof(version), 1, file);
int size = data.GetNumberOfBytesUsed();
fwrite(&size, sizeof(size), 1, file);
fwrite(data.GetData(), 1, size, file);
fclose(file);
return true;
}
bool ExternalFileLevelStorage::readPlayerData(const std::string& filename, LevelData& dest)
{
FILE* fp = fopen(filename.c_str(), "rb");
if (!fp)
return false;
do {
int version;
if (fread(&version, 4, 1, fp) != 1)
break;
int size;
if (fread(&size, 4, 1, fp) != 1)
break;
if (version == 1) {
if (fread(&dest.playerData, 1, sizeof(dest.playerData), fp) != size)
break;
// Fix coordinates
Vec3& pos = dest.playerData.pos;
if (pos.x < 0.5f) pos.x = 0.5f;
if (pos.z < 0.5f) pos.z = 0.5f;
if (pos.x > (LEVEL_WIDTH - 0.5f)) pos.x = LEVEL_WIDTH - 0.5f;
if (pos.z > (LEVEL_DEPTH - 0.5f)) pos.z = LEVEL_DEPTH - 0.5f;
if (pos.y < 0) pos.y = 64;
dest.playerDataVersion = version;
}
} while (false);
fclose(fp);
return true;
}
void ExternalFileLevelStorage::tick()
{
tickCount++;
if ((tickCount % 1000) == 0 && level) {
LOGI("Saving level...\n");
// look for chunks that needs to be saved
for (int z = 0; z < CHUNK_CACHE_WIDTH; z++)
{
for (int x = 0; x < CHUNK_CACHE_WIDTH; x++)
{
LevelChunk* chunk = level->getChunk(x, z);
if (chunk && chunk->unsaved)
{
int pos = x + z * CHUNK_CACHE_WIDTH;
UnsavedChunkList::iterator prev = unsavedChunkList.begin();
for ( ; prev != unsavedChunkList.end(); ++prev)
{
if ((*prev).pos == pos)
{
// the chunk has been modified again, so update its time
(*prev).addedToList = RakNet::GetTimeMS();
break;
}
}
if (prev == unsavedChunkList.end())
{
UnsavedLevelChunk unsaved;
unsaved.pos = pos;
unsaved.addedToList = RakNet::GetTimeMS();
unsaved.chunk = chunk;
unsavedChunkList.push_back(unsaved);
}
chunk->unsaved = false; // not actually saved, but in our working list at least
}
}
}
savePendingUnsavedChunks(2);
}
if (tickCount - lastSavedEntitiesTick > (60 * SharedConstants::TicksPerSecond)) {
saveEntities(level, NULL);
}
}
void ExternalFileLevelStorage::save(Level* level, LevelChunk* levelChunk)
{
if (!regionFile)
{
regionFile = new RegionFile(levelPath);
if (!regionFile->open())
{
delete regionFile;
regionFile = NULL;
return;
}
}
// Write chunk
RakNet::BitStream chunkData;
chunkData.Write((const char*)levelChunk->getBlockData(), CHUNK_BLOCK_COUNT);
chunkData.Write((const char*)levelChunk->data.data, CHUNK_BLOCK_COUNT / 2);
chunkData.Write((const char*)levelChunk->skyLight.data, CHUNK_BLOCK_COUNT / 2);
chunkData.Write((const char*)levelChunk->blockLight.data, CHUNK_BLOCK_COUNT / 2);
chunkData.Write((const char*)levelChunk->updateMap, CHUNK_COLUMNS);
regionFile->writeChunk(levelChunk->x, levelChunk->z, chunkData);
// Write entities
//LOGI("Saved chunk (%d, %d)\n", levelChunk->x, levelChunk->z);
}
LevelChunk* ExternalFileLevelStorage::load(Level* level, int x, int z)
{
if (!regionFile)
{
regionFile = new RegionFile(levelPath);
if (!regionFile->open())
{
delete regionFile;
regionFile = NULL;
return NULL;
}
}
RakNet::BitStream* chunkData = NULL;
if (!regionFile->readChunk(x, z, &chunkData))
{
//LOGI("Failed to read data for %d, %d\n", x, z);
return NULL;
}
chunkData->ResetReadPointer();
unsigned char* blockIds = new unsigned char[CHUNK_BLOCK_COUNT];
chunkData->Read((char*)blockIds, CHUNK_BLOCK_COUNT);
LevelChunk* levelChunk = new LevelChunk(level, blockIds, x, z);
chunkData->Read((char*)levelChunk->data.data, CHUNK_BLOCK_COUNT / 2);
if (loadedStorageVersion >= ChunkVersion_Light) {
chunkData->Read((char*)levelChunk->skyLight.data, CHUNK_BLOCK_COUNT / 2);
chunkData->Read((char*)levelChunk->blockLight.data, CHUNK_BLOCK_COUNT / 2);
}
chunkData->Read((char*)levelChunk->updateMap, CHUNK_COLUMNS);
// This will be difficult to maintain.. Storage version could be per chunk
// too (but probably better to just read all -> write all, so that all
// chunks got same version anyway)
//if (loadedStorageVersion >= ChunkVersion_Entity) {
// int dictSize;
// chunkData->Read(dictSize);
// RakDataInput dis(*chunkData);
// Tag* tmp = Tag::readNamedTag(&dis);
// if (tmp && tmp->getId() == Tag::TAG_Compound) {
// CompoundTag* tag = (CompoundTag*) tmp;
// delete tmp;
// }
//}
delete [] chunkData->GetData();
delete chunkData;
//bool dbg = (x == 7 && z == 9);
//int t = 0;
//for (int i = 0; i < CHUNK_COLUMNS; ++i) {
// char bits = levelChunk->updateMap[i];
// t += (bits != 0);
// int xx = x * 16 + i%16;
// int zz = z * 16 + i/16;
// if (dbg && xx == 125 && zz == 152) {
// LOGI("xz: %d, %d: %d\n", xx, zz, bits);
// for (int j = 0; j < 8; ++j) {
// if (bits & (1 << j)) {
// LOGI("%d - %d\n", j << 4, ((j+1) << 4) - 1);
// }
// }
// }
//}
//
// Convert LevelChunks here if necessary
//
//LOGI("level version: %d: upd: %d - (%d, %d)\n", loadedStorageVersion, t, x, z);
bool changed = false;
// Loaded level has old Cloth types (one Tile* per color)
if (loadedStorageVersion == 1)
changed |= LevelConverters::v1_ClothIdToClothData(levelChunk);
// Loaded level is newer than our level - replace all unavailable block types
//if (loadedStorageVersion > SharedConstants::StorageVersion)
changed |= LevelConverters::ReplaceUnavailableBlocks(levelChunk);
levelChunk->recalcHeightmap();
levelChunk->unsaved = changed;
levelChunk->terrainPopulated = true;
levelChunk->createdFromSave = true;
return levelChunk;
}
void ExternalFileLevelStorage::saveEntities( Level* level, LevelChunk* levelChunk )
{
lastSavedEntitiesTick = tickCount;
int count = 0;
float st = getTimeS();
// Version 1: Save ALL Entities for all chunks in one structure
EntityList& entities = level->entities;
ListTag* entityTags = new ListTag();
for (unsigned int i = 0; i < entities.size(); ++i) {
Entity* e = entities[i];
CompoundTag* tag = new CompoundTag();
if (e->save(tag)) {
count++;
entityTags->add(tag);
} else
delete tag;
}
// Version 1: Save ALL TileEntities for all chunks in one structure
TileEntityList& tileEntities = level->tileEntities;
//TileEntityList keep, dontKeep;
//partitionTileEntities
ListTag* tileEntityTags = new ListTag();
for (unsigned int i = 0; i < tileEntities.size(); ++i) {
TileEntity* e = tileEntities[i];
if (!e->shouldSave()) continue;
CompoundTag* tag = new CompoundTag();
if (e->save(tag)) {
count++;
tileEntityTags->add(tag);
} else
delete tag;
}
CompoundTag base;
base.put("Entities", entityTags);
base.put("TileEntities", tileEntityTags);
RakNet::BitStream stream;
RakDataOutput dos(stream);
NbtIo::write(&base, &dos);
int numBytes = stream.GetNumberOfBytesUsed();
FILE* fp = fopen((levelPath + "/entities.dat").c_str(), "wb");
if (fp) {
int version = 1;
fwrite("ENT\0", 1, 4, fp);
fwrite(&version, sizeof(int), 1, fp);
fwrite(&numBytes, sizeof(int), 1, fp);
fwrite(stream.GetData(), 1, numBytes, fp);
fclose(fp);
}
base.deleteChildren();
float tt = getTimeS() - st;
LOGI("Time to save %d entities: %f s. Size: %d bytes\n", count, tt, numBytes);
}
void ExternalFileLevelStorage::loadEntities(Level* level, LevelChunk* chunk) {
lastSavedEntitiesTick = tickCount;
FILE* fp = fopen((levelPath + "/entities.dat").c_str(), "rb");
if (fp) {
char header[5];
int version, numBytes;
fread(header, 1, 4, fp);
fread(&version, sizeof(int), 1, fp);
fread(&numBytes, sizeof(int), 1, fp);
int left = getRemainingFileSize(fp);
if (numBytes <= left && numBytes > 0) {
unsigned char* buf = new unsigned char[numBytes];
fread(buf, 1, numBytes, fp);
RakNet::BitStream stream(buf, numBytes, false);
RakDataInput dis(stream);
CompoundTag* tag = NbtIo::read(&dis);
//
// Read Entity:es
//
if (tag->contains("Entities", Tag::TAG_List)) {
ListTag* entityTags = tag->getList("Entities");
for (int i = 0; i < entityTags->size(); ++i) {
Tag* _et = entityTags->get(i);
if (!_et || _et->getId() != Tag::TAG_Compound) {
LOGE("Entity tag is either NULL or not a compoundTag: %p : %d!\n", _et, _et?_et->getId() : -1);
continue;
}
CompoundTag* et = (CompoundTag*)_et;
if (Entity* e = EntityFactory::loadEntity(et, level)) {
level->addEntity(e);
}
}
}
//
// Read TileEntity:s
//
if (tag->contains("TileEntities", Tag::TAG_List)) {
ListTag* tileEntityTags = tag->getList("TileEntities");
for (int i = 0; i < tileEntityTags->size(); ++i) {
Tag* _et = tileEntityTags->get(i);
if (!_et || _et->getId() != Tag::TAG_Compound) {
LOGE("TileEntity tag is either NULL or not a compoundTag: %p : %d!\n", _et, _et?_et->getId() : -1);
continue;
}
CompoundTag* et = (CompoundTag*)_et;
if (TileEntity* e = TileEntity::loadStatic(et)) {
LevelChunk* chunk = level->getChunkAt(e->x, e->z);
if (chunk && !chunk->hasTileEntityAt(e)) {
LOGI("Adding TileEntity %d to %d, %d, %d\n", e->type, e->x, e->y, e->z);
chunk->addTileEntity(e);
} else {
if (!chunk)
LOGE("Couldn't find chunk at %d, %d to add %d\n", e->x, e->z, e->type);
else
LOGE("Already have TileEntity at %d, %d to add %d\n", e->x, e->z, e->type);
delete e;
}
}
}
}
tag->deleteChildren();
delete tag;
delete[] buf;
}
LOGI("header: %s, version: %d, bytes: %d (remaining: %d)\n", header, version, numBytes, left);
//fread(stream.GetData(), 1, numBytes, fp);
fclose(fp);
}
}
void ExternalFileLevelStorage::saveGame(Level* level) {
saveEntities(level, NULL);
}
int ExternalFileLevelStorage::savePendingUnsavedChunks( int maxCount ) {
if (maxCount < 0)
maxCount = unsavedChunkList.size();
int count = 0;
while (++count <= maxCount && !unsavedChunkList.empty()) {
UnsavedChunkList::iterator it = unsavedChunkList.begin();
UnsavedChunkList::iterator remove = unsavedChunkList.begin();
UnsavedLevelChunk* oldest = &(*it);
for ( ; it != unsavedChunkList.end(); ++it) {
if ((*it).addedToList < oldest->addedToList) {
oldest = &(*it);
remove = it;
}
}
LevelChunk* chunk = oldest->chunk;
unsavedChunkList.erase(remove);
save(level, chunk);
}
return count;
}
void ExternalFileLevelStorage::saveAll( Level* level, std::vector<LevelChunk*>& levelChunks ) {
ChunkStorage::saveAll(level, levelChunks);
int numChunks = savePendingUnsavedChunks(-1);
LOGI("Saving %d additional chunks.\n", numChunks);
}
#endif /*DEMO_MODE*/