diff options
Diffstat (limited to 'archived/projt-launcher/launcher/minecraft/World.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/minecraft/World.cpp | 652 |
1 files changed, 652 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/minecraft/World.cpp b/archived/projt-launcher/launcher/minecraft/World.cpp new file mode 100644 index 0000000000..1fc040db9c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/World.cpp @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * 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 + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * 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 + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "World.h" +#include <QDebug> +#include <QDir> +#include <QDirIterator> +#include <QString> + +#include <FileSystem.h> +#include <MMCZip.h> +#include <io/stream_reader.h> +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> +#include <tag_primitive.h> +#include <tag_string.h> +#include <sstream> +#include "GZip.h" + +#include <QCoreApplication> + +#include <optional> + +#include "FileSystem.h" +#include "PSaveFile.h" + +using std::nullopt; +using std::optional; + +GameType::GameType(std::optional<int> original) : original(original) +{ + if (!original) + { + return; + } + switch (*original) + { + case 0: type = GameType::Survival; break; + case 1: type = GameType::Creative; break; + case 2: type = GameType::Adventure; break; + case 3: type = GameType::Spectator; break; + default: break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: return QCoreApplication::translate("GameType", "Spectator"); + default: break; + } + if (original) + { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: return "Survival"; + case GameType::Creative: return "Creative"; + case GameType::Adventure: return "Adventure"; + case GameType::Spectator: return "Spectator"; + default: break; + } + if (original) + { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + +std::unique_ptr<nbt::tag_compound> parseLevelDat(QByteArray data) +{ + QByteArray output; + if (!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + try + { + auto pair = nbt::io::read_compound(foo); + + if (pair.first != "") + return nullptr; + + if (pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (const nbt::io::input_error& e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } +} + +QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int)s.str().size()); + return val; +} + +QString getDatFromFS(const QFileInfo& root, QString file) +{ + QDir worldDir(root.filePath()); + if (!root.isDir() || !worldDir.exists(file)) + { + return QString(); + } + return worldDir.absoluteFilePath(file); +} + +QString getLevelDatFromFS(const QFileInfo& file) +{ + return getDatFromFS(file, "level.dat"); +} + +QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +{ + auto fullFilePath = getDatFromFS(root, file); + if (fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if (!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "level.dat"); +} + +QByteArray getWorldGenDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); +} + +bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if (fullFilePath.isNull()) + { + return false; + } + PSaveFile f(fullFilePath); + if (!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if (!GZip::zip(data, compressed)) + { + return false; + } + if (f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo& file) +{ + repath(file); +} + +void World::repath(const QFileInfo& file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if (file.isFile() && file.suffix() == "zip") + { + m_iconFile = QString(); + readFromZip(file); + } + else if (file.isDir()) + { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if (assumedIconPath.exists()) + { + m_iconFile = assumedIconPath.absoluteFilePath(); + } + readFromFS(file); + } +} + +bool World::resetIcon() +{ + if (m_iconFile.isNull()) + { + return false; + } + if (QFile(m_iconFile).remove()) + { + m_iconFile = QString(); + return true; + } + return false; +} + +int64_t loadSeed(QByteArray data); + +void World::readFromFS(const QFileInfo& file) +{ + auto bytes = getLevelDatDataFromFS(file); + if (bytes.isEmpty()) + { + m_isValid = false; + return; + } + loadFromLevelDat(bytes); + m_levelDatTime = file.lastModified(); + if (m_randomSeed == 0) + { + auto worldGenBytes = getWorldGenDataFromFS(file); + if (!worldGenBytes.isEmpty()) + { + m_randomSeed = loadSeed(worldGenBytes); + } + } +} + +void World::readFromZip(const QFileInfo& file) +{ + QuaZip zip(file.absoluteFilePath()); + m_isValid = zip.open(QuaZip::mdUnzip); + if (!m_isValid) + { + return; + } + auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + m_isValid = !location.isEmpty(); + if (!m_isValid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + m_isValid = zip.setCurrentFile(location + "level.dat"); + if (!m_isValid) + { + return; + } + m_isValid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if (!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + m_levelDatTime = modTime; + if (!m_isValid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString& to, const QString& name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if (!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + bool ok = false; + if (m_containerFile.isFile()) + { + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); + } + else if (m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if (ok && !name.isEmpty() && m_actualName != name) + { + QFileInfo finalPathInfo(finalPath); + World newWorld(finalPathInfo); + if (newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString& newName) +{ + if (m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if (data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if (!worldData) + { + return false; + } + auto& val = worldData->at("Data"); + if (val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto& dataCompound = val.as<nbt::tag_compound>(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +namespace +{ + + optional<QString> read_string(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::String) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_string>(); + return QString::fromStdString(tag_str.get()); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; + } + } + + optional<int64_t> read_long(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Long) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_long>(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; + } + } + + optional<int> read_int(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Int) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_int>(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } + } + + GameType read_gametype(nbt::value& parent, const char* name) + { + return GameType(read_int(parent, name)); + } + +} // namespace + +int64_t loadSeed(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + return 0; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("data"); + } + catch (const std::out_of_range&) + { + return 0; + } + nbt::value& val = *valPtr; + + try + { + return read_long(val, "seed").value_or(0); + } + catch (const std::out_of_range&) + { + } + return 0; +} + +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + m_isValid = false; + return; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("Data"); + } + catch (const std::out_of_range& e) + { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + m_isValid = false; + return; + } + nbt::value& val = *valPtr; + + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional<int64_t> randomSeed; + try + { + auto& WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range&) + {} + if (!randomSeed) + { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if (randomSeed) + { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "Size:" << m_size; + qDebug() << "GameType:" << m_gameType.toLogString(); +} + +bool World::replace(World& with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if (!m_isValid) + return false; + + if (FS::trash(m_containerFile.filePath())) + return true; + + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if (m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World& other) const +{ + return m_isValid == other.m_isValid && folderName() == other.folderName(); +} + +bool World::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool World::isMoreThanOneHardLink() const +{ + if (m_containerFile.isDir()) + { + return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; + } + return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; +} + +void World::setSize(int64_t size) +{ + m_size = size; +} |
