diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/minecraft/World.cpp | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/minecraft/World.cpp')
| -rw-r--r-- | meshmc/launcher/minecraft/World.cpp | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/World.cpp b/meshmc/launcher/minecraft/World.cpp new file mode 100644 index 0000000000..4ae59afaf5 --- /dev/null +++ b/meshmc/launcher/minecraft/World.cpp @@ -0,0 +1,478 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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, see <https://www.gnu.org/licenses/>. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2015-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 <QDir> +#include <QString> +#include <QDebug> +#include <QSaveFile> +#include "World.h" + +#include "GZip.h" +#include <MMCZip.h> +#include <FileSystem.h> +#include <sstream> +#include <io/stream_reader.h> +#include <tag_string.h> +#include <tag_primitive.h> + +#include <QCoreApplication> + +#include <nonstd/optional> + +using nonstd::nullopt; +using nonstd::optional; + +GameType::GameType(nonstd::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 getLevelDatFromFS(const QFileInfo& file) +{ + QDir worldDir(file.filePath()); + if (!file.isDir() || !worldDir.exists("level.dat")) { + return QString(); + } + return worldDir.absoluteFilePath("level.dat"); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + auto fullFilePath = getLevelDatFromFS(file); + if (fullFilePath.isNull()) { + return QByteArray(); + } + QFile f(fullFilePath); + if (!f.open(QIODevice::ReadOnly)) { + return QByteArray(); + } + return f.readAll(); +} + +bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if (fullFilePath.isNull()) { + return false; + } + QSaveFile 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; +} + +void World::readFromFS(const QFileInfo& file) +{ + auto bytes = getLevelDatDataFromFS(file); + if (bytes.isEmpty()) { + is_valid = false; + return; + } + loadFromLevelDat(bytes); + levelDatTime = file.lastModified(); +} + +void World::readFromZip(const QFileInfo& file) +{ + QString zipPath = file.absoluteFilePath(); + auto location = MMCZip::findFolderOfFileInZip(zipPath, "level.dat"); + is_valid = !location.isEmpty(); + if (!is_valid) { + return; + } + m_containerOffsetPath = location; + QByteArray levelDatData = + MMCZip::readFileFromZip(zipPath, location + "level.dat"); + is_valid = !levelDatData.isEmpty(); + if (!is_valid) { + return; + } + levelDatTime = MMCZip::getEntryModTime(zipPath, location + "level.dat"); + loadFromLevelDat(levelDatData); +} + +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()) { + auto result = MMCZip::extractSubDir(m_containerFile.absoluteFilePath(), + m_containerOffsetPath, finalPath); + ok = result.has_value(); + } else if (m_containerFile.isDir()) { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if (ok && !name.isEmpty() && m_actualName != name) { + World newWorld{QFileInfo(finalPath)}; + 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 (const std::out_of_range& e) { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; + } catch (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 (const std::out_of_range& e) { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; + } catch (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 (const std::out_of_range& e) { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } catch (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 + +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) { + is_valid = 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(); + is_valid = false; + return; + } + nbt::value& val = *valPtr; + + is_valid = val.get_type() == nbt::tag_type::Compound; + if (!is_valid) + 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) : 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() << "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 (!is_valid) + return false; + 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 is_valid == other.is_valid && folderName() == other.folderName(); +} |
