├── .clang-format ├── .github ├── FUNDING.yml └── workflows │ ├── benchmark.yml │ ├── format-police.yml │ └── routine-exam.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets └── sample.png ├── contrib ├── leaflet │ └── index.html └── openlayers │ ├── README.md │ ├── mcmap.html │ └── render.sh ├── package-debian ├── Makefile ├── build-debian.dockerfile └── nfpm.yaml ├── scripts ├── CMakeLists.txt ├── README.md ├── average.sh ├── chunkPos.cpp ├── extractChunk.cpp ├── json2bson.cpp ├── nbt2json.cpp └── regionReader.cpp ├── src ├── CMakeLists.txt ├── VERSION ├── block_drawers.cpp ├── block_drawers.h ├── blocktypes.def ├── canvas.cpp ├── canvas.h ├── chunk.cpp ├── chunk.h ├── chunk_format_versions │ ├── assert.cpp │ ├── assert.hpp │ ├── get_section.cpp │ ├── get_section.hpp │ ├── section_format.cpp │ └── section_format.hpp ├── cli │ ├── CMakeLists.txt │ ├── cli.cpp │ ├── parse.cpp │ └── parse.h ├── colors.cpp ├── colors.h ├── colors.json ├── graphical │ ├── CMakeLists.txt │ ├── icons │ │ ├── deepslate_diamond.png │ │ ├── deepslate_redstone.png │ │ ├── grass_block.png │ │ ├── lapis.png │ │ ├── lava.png │ │ └── sprout.png │ ├── main.cpp │ ├── mainwindow.cpp │ ├── mainwindow.h │ └── mainwindow.ui ├── helper.cpp ├── helper.h ├── include │ ├── 2DCoordinates.hpp │ ├── CMakeLists.txt │ ├── compat.hpp │ ├── counter.hpp │ ├── logger.hpp │ ├── map.hpp │ ├── nbt │ │ ├── iterators.hpp │ │ ├── nbt.hpp │ │ ├── parser.hpp │ │ ├── stream.hpp │ │ ├── tag_types.hpp │ │ ├── to_json.hpp │ │ └── writer.hpp │ ├── progress.hpp │ └── translator.hpp ├── mcmap.cpp ├── mcmap.h ├── png.cpp ├── png.h ├── region.cpp ├── region.h ├── savefile.cpp ├── savefile.h ├── section.cpp ├── section.h ├── settings.cpp ├── settings.h ├── worldloader.cpp └── worldloader.h └── tests ├── CMakeLists.txt ├── README.md ├── sample_chunk.cpp ├── sample_level.cpp ├── samples.h ├── test_canvas.cpp ├── test_chunk.cpp ├── test_colors.cpp ├── test_compat.cpp ├── test_coordinates.cpp ├── test_nbt.cpp └── test_section.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Right 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: All 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: MultiLine 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 80 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: false 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Preserve 70 | IncludeCategories: 71 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 75 | Priority: 3 76 | SortPriority: 0 77 | - Regex: '.*' 78 | Priority: 1 79 | SortPriority: 0 80 | IncludeIsMainRegex: '(Test)?$' 81 | IncludeIsMainSourceRegex: '' 82 | IndentCaseLabels: false 83 | IndentGotoLabels: true 84 | IndentPPDirectives: None 85 | IndentWidth: 2 86 | IndentWrappedFunctionNames: false 87 | JavaScriptQuotes: Leave 88 | JavaScriptWrapImports: true 89 | KeepEmptyLinesAtTheStartOfBlocks: true 90 | MacroBlockBegin: '' 91 | MacroBlockEnd: '' 92 | MaxEmptyLinesToKeep: 1 93 | NamespaceIndentation: None 94 | ObjCBinPackProtocolList: Auto 95 | ObjCBlockIndentWidth: 2 96 | ObjCSpaceAfterProperty: false 97 | ObjCSpaceBeforeProtocolList: true 98 | PenaltyBreakAssignment: 2 99 | PenaltyBreakBeforeFirstCallParameter: 19 100 | PenaltyBreakComment: 300 101 | PenaltyBreakFirstLessLess: 120 102 | PenaltyBreakString: 1000 103 | PenaltyBreakTemplateDeclaration: 10 104 | PenaltyExcessCharacter: 1000000 105 | PenaltyReturnTypeOnItsOwnLine: 60 106 | PointerAlignment: Right 107 | ReflowComments: true 108 | SortIncludes: true 109 | SortUsingDeclarations: true 110 | SpaceAfterCStyleCast: false 111 | SpaceAfterLogicalNot: false 112 | SpaceAfterTemplateKeyword: true 113 | SpaceBeforeAssignmentOperators: true 114 | SpaceBeforeCpp11BracedList: false 115 | SpaceBeforeCtorInitializerColon: true 116 | SpaceBeforeInheritanceColon: true 117 | SpaceBeforeParens: ControlStatements 118 | SpaceBeforeRangeBasedForLoopColon: true 119 | SpaceInEmptyBlock: false 120 | SpaceInEmptyParentheses: false 121 | SpacesBeforeTrailingComments: 1 122 | SpacesInAngles: false 123 | SpacesInConditionalStatement: false 124 | SpacesInContainerLiterals: true 125 | SpacesInCStyleCastParentheses: false 126 | SpacesInParentheses: false 127 | SpacesInSquareBrackets: false 128 | SpaceBeforeSquareBrackets: false 129 | Standard: Latest 130 | StatementMacros: 131 | - Q_UNUSED 132 | - QT_REQUIRE_VERSION 133 | TabWidth: 8 134 | UseCRLF: false 135 | UseTab: Never 136 | ... 137 | 138 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: spoutn1k 2 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Commando Training 2 | on: [push, pull_request] 3 | jobs: 4 | Benchmark: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install dependencies 9 | run: sudo apt-get install -y libfmt-dev libspdlog-dev libpng-dev 10 | - name: Compilation and execution 11 | uses: spoutn1k/mcmap-benchmark@cmake 12 | - name: Upload time data 13 | uses: actions/upload-artifact@v4 14 | with: 15 | name: results.log 16 | path: time.log 17 | - name: Upload results 18 | uses: actions/upload-artifact@v4 19 | with: 20 | name: images.tgz 21 | path: images.tgz 22 | -------------------------------------------------------------------------------- /.github/workflows/format-police.yml: -------------------------------------------------------------------------------- 1 | name: Format Police 2 | on: [push, pull_request] 3 | jobs: 4 | formatting-check: 5 | name: Formatting Check 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | path: 10 | - 'src' 11 | - 'scripts' 12 | - 'tests' 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Run clang-format 16 | uses: jidicula/clang-format-action@v4.5.0 17 | with: 18 | clang-format-version: '13' 19 | check-path: ${{ matrix.path }} 20 | -------------------------------------------------------------------------------- /.github/workflows/routine-exam.yml: -------------------------------------------------------------------------------- 1 | name: Routine Exam 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | unit-tests: 6 | name: Unit Tests 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Install dependencies 10 | run: sudo apt-get install -y libgtest-dev libfmt-dev libspdlog-dev libpng-dev 11 | && cd /usr/src/gtest 12 | && CXX=g++ sudo cmake CMakeLists.txt 13 | && sudo make -j 14 | && sudo cp lib/*a /usr/lib 15 | && sudo ln -s /usr/lib/libgtest.a /usr/local/lib/libgtest.a 16 | && sudo ln -s /usr/lib/libgtest_main.a /usr/local/lib/libgtest_main.a 17 | - uses: actions/checkout@v2 18 | - name: Configure project 19 | run: mkdir build 20 | && cd build 21 | && CXX=g++ cmake .. 22 | - name: Build project 23 | run: cd build 24 | && make -j run_tests 25 | - name: Run tests 26 | run: ./build/bin/run_tests 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Output files 2 | *.o 3 | *.png 4 | !sample.png 5 | src/colors.bson 6 | src/graphical/ui_mainwindow.h 7 | src/graphical/icons.qrc 8 | src/include/json.hpp 9 | src/include/fmt/core.h 10 | src/include/fmt/format.h 11 | src/include/fmt/format-inl.h 12 | src/include/fmt/color.h 13 | src/include/fmt/format.cc 14 | 15 | # Tools byproducts 16 | .gdb_history 17 | .ycm_extra_conf.py 18 | */tags 19 | CMakeCache.txt 20 | CMakeFiles/ 21 | Makefile 22 | cmake_install.cmake 23 | build 24 | package-debian/out 25 | 26 | #IDE 27 | .idea/ 28 | *.iml 29 | 30 | # Executables 31 | mcmap 32 | run_tests 33 | *json2bson 34 | *extractChunk 35 | *nbt2json 36 | *regionReader 37 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | CMAKE_MINIMUM_REQUIRED(VERSION 3.0) 2 | 3 | PROJECT(mcmap LANGUAGES CXX VERSION 3.0.1) 4 | 5 | SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 6 | 7 | OPTION(DEBUG_BUILD "Debug build" OFF) 8 | 9 | IF(STATIC_BUILD) 10 | SET(CMAKE_FIND_LIBRARY_SUFFIXES ".a") 11 | SET(BUILD_SHARED_LIBS OFF) 12 | SET(CMAKE_EXE_LINKER_FLAGS "-static") 13 | ENDIF() 14 | 15 | FIND_PACKAGE(PNG REQUIRED) 16 | FIND_PACKAGE(ZLIB REQUIRED) 17 | FIND_PACKAGE(fmt REQUIRED) 18 | FIND_PACKAGE(spdlog REQUIRED) 19 | FIND_PACKAGE(OpenMP) 20 | FIND_PACKAGE(GTest) 21 | FIND_PACKAGE(Qt6 COMPONENTS Widgets LinguistTools) 22 | FIND_PACKAGE(Git) 23 | 24 | IF (Git_FOUND) 25 | # Copy the git index to a random unused file to trigger a reconfigure 26 | # when it changes: this allows to always have an up-to-date string in 27 | # GIT_DESCRIBE set below 28 | CONFIGURE_FILE( 29 | "${PROJECT_SOURCE_DIR}/.git/index" 30 | "${CMAKE_BINARY_DIR}/.git_index" 31 | COPYONLY) 32 | 33 | EXECUTE_PROCESS( 34 | COMMAND "${GIT_EXECUTABLE}" describe --always HEAD 35 | WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" 36 | RESULT_VARIABLE res 37 | OUTPUT_VARIABLE GIT_DESCRIBE 38 | ERROR_QUIET 39 | OUTPUT_STRIP_TRAILING_WHITESPACE) 40 | ENDIF() 41 | 42 | SET(CMAKE_CXX_STANDARD 17) 43 | SET(CMAKE_CXX_STANDARD_REQUIRED ON) 44 | 45 | INCLUDE_DIRECTORIES(src/include) 46 | 47 | IF (NOT WIN32) 48 | ADD_COMPILE_OPTIONS(-Wall -Wextra -pedantic) 49 | ENDIF() 50 | 51 | ADD_DEFINITIONS( 52 | -DSPDLOG_FMT_EXTERNAL=1 53 | -D_FILE_OFFSET_BITS=64 54 | -DCXX_COMPILER_ID="${CMAKE_CXX_COMPILER_ID}" 55 | -DCXX_COMPILER_VERSION="${CMAKE_CXX_COMPILER_VERSION}" 56 | ) 57 | 58 | IF(DEBUG_BUILD) 59 | ADD_COMPILE_OPTIONS(-O0 -g3) 60 | ADD_DEFINITIONS(-DDEBUG_BUILD) 61 | ELSE() 62 | ADD_COMPILE_OPTIONS(-O3) 63 | ENDIF() 64 | 65 | IF(WIN32) 66 | # 100M stack + 3.5G heap on windows 67 | SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:104857600 /HEAP:3758096384") 68 | ADD_DEFINITIONS(-D_WINDOWS) 69 | ENDIF() 70 | 71 | IF(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 72 | IF(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0.0") 73 | ADD_LINK_OPTIONS(-lstdc++fs) 74 | ENDIF() 75 | ENDIF() 76 | 77 | # Get file URL DEST MD5 78 | FUNCTION(GET_FILE) 79 | FILE( 80 | DOWNLOAD 81 | ${ARGV0} 82 | ${ARGV1} 83 | EXPECTED_HASH ${ARGV2} 84 | STATUS DL_STATUS 85 | ) 86 | 87 | LIST(GET DL_STATUS 0 _STATUS) 88 | 89 | IF(_STATUS) 90 | LIST(GET DL_STATUS 1 _ERROR) 91 | MESSAGE("Error downloading ${ARGV0}: ${_ERROR}") 92 | ELSE() 93 | MESSAGE("-- Successfully downloaded ${ARGV0}") 94 | ENDIF() 95 | ENDFUNCTION() 96 | 97 | 98 | ADD_SUBDIRECTORY(src) 99 | ADD_SUBDIRECTORY(scripts) 100 | ADD_SUBDIRECTORY(tests) 101 | -------------------------------------------------------------------------------- /assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/assets/sample.png -------------------------------------------------------------------------------- /contrib/leaflet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 71 | 72 | -------------------------------------------------------------------------------- /contrib/openlayers/README.md: -------------------------------------------------------------------------------- 1 | #OpenLayers + mcmap# 2 | This is a simple example of how you could start using mcmap with [OpenLayers](http://openlayers.org). 3 | OpenLayers makes it easy to put a dynamic map in any web page. It can display map tiles and markers loaded from any source. 4 | OpenLayers has been developed to further the use of geographic information of all kinds. 5 | OpenLayers is completely free, Open Source JavaScript, released under the 2-clause BSD License (also known as the FreeBSD). 6 | 7 | 8 | ##Instructions## 9 | This will work on Linux if you have ImageMagic and pngcrush installed. 10 | 11 | * Make sure mcmap is in your path or edit render.sh 12 | * $>./render.sh 13 | * open mcmap.html in a web browser. 14 | The browser must support loading from local files or you have to put mcmap.html and the tiles folder that will be crated on a web server 15 | 16 | 17 | 18 | ##TODO## 19 | * Make stuff use some sort of usable coordinate system and don't let openlayers guess it 20 | 21 | -------------------------------------------------------------------------------- /contrib/openlayers/mcmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | mcmap Sample 4 | 5 | 6 | 30 | 31 | 32 |

Zoom:

33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /contrib/openlayers/render.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #first argument should be the world 4 | WORLD=$1 5 | # where can we find mcmap 6 | MCMAPBIN="mcmap" 7 | # where shoud we put the processed tiles 8 | TILESDIR="`pwd`/tiles" 9 | # where do we put the raw tiles 10 | RAWTILESDIR="`pwd`/tmp" 11 | 12 | function usage(){ 13 | echo "Usage:" 14 | echo $0 "" 15 | } 16 | 17 | if [[ ! -d $WORLD ]] ; then 18 | usage 19 | exit 1 20 | fi 21 | 22 | #make some folders to store stuff 23 | if [[ ! -d $TILESDIR ]] ; then 24 | mkdir -p $TILESDIR 25 | fi 26 | if [[ ! -d $RAWTILESDIR ]] ; then 27 | mkdir -p $RAWTILESDIR 28 | fi 29 | 30 | 31 | #generate the raw tiles 32 | $MCMAPBIN -split $RAWTILESDIR $WORLD 33 | 34 | #process all raw tiles 35 | echo "Processing raw tiles" 36 | for SRC in $RAWTILESDIR/*.png ; do 37 | FILE="`basename $SRC`" 38 | #default tile size for OpenLayers is 256x256 pixels 39 | convert -scale 256 $SRC $TILESDIR/$FILE 40 | done 41 | echo "Done" 42 | 43 | -------------------------------------------------------------------------------- /package-debian/Makefile: -------------------------------------------------------------------------------- 1 | COMMIT_SHA_SHORT ?= $(shell git rev-parse --short=12 HEAD) 2 | PWD_DIR:= ${CURDIR} 3 | SHELL := /bin/bash 4 | 5 | default: help; 6 | 7 | # ====================================================================================== 8 | build-builder-debian: ## build the builder image that contains the source code 9 | @docker build -f build-debian.dockerfile -t mcmap-builder-debian:latest ./.. 10 | 11 | build-debian: build-builder-debian ## build for linux using docker 12 | @mkdir -p out 13 | @docker run -it --rm -v ${PWD_DIR}/out:/out mcmap-builder-debian:latest /bin/bash -c "mkdir -p /mcmap/build && \ 14 | cd /mcmap/build && \ 15 | cmake .. && \ 16 | make -j mcmap mcmap-gui && \ 17 | cp /mcmap/build/bin/* /out" 18 | 19 | package-debian: build-debian ## package the just compiled binary 20 | @docker run -it --rm -v ${PWD_DIR}/out:/out mcmap-builder-debian:latest /bin/bash -c "cd /out && \ 21 | nfpm pkg -f /mcmap/package-debian/nfpm.yaml -p deb" 22 | 23 | 24 | help: ## Show this help 25 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m- %-20s\033[0m %s\n", $$1, $$2}' 26 | -------------------------------------------------------------------------------- /package-debian/build-debian.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.10 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ENV TZ=Etc/PDT 5 | RUN apt-get update && apt-get install -y \ 6 | git make g++ libpng-dev cmake libspdlog-dev qttools5-dev 7 | 8 | # install nfpm 9 | RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list && \ 10 | apt-get update && apt-get install -y nfpm 11 | 12 | COPY . /mcmap 13 | -------------------------------------------------------------------------------- /package-debian/nfpm.yaml: -------------------------------------------------------------------------------- 1 | # mcmap packaging config 2 | # expects to be run from the `bin` directory 3 | 4 | name: "mcmap" 5 | arch: "amd64" 6 | platform: "linux" 7 | version: "3.0.2" 8 | section: "default" 9 | priority: "extra" 10 | description: | 11 | Mcmap is a tool allowing you to create isometric renders of your Minecraft save file. 12 | maintainer: "Andres Bott " 13 | homepage: "https://github.com/spoutn1k/mcmap" 14 | license: "GPL-3.0 license" 15 | contents: 16 | - src: ./mcmap # this path is mounted into the container 17 | dst: /usr/local/bin/mcmap 18 | file_info: 19 | mode: 0755 20 | owner: root 21 | group: root 22 | - src: ./mcmap-gui 23 | dst: /usr/local/bin/mcmap-gui 24 | file_info: 25 | mode: 0755 26 | owner: root 27 | group: root 28 | overrides: 29 | deb: 30 | depends: 31 | - libpng16-16 32 | - libgomp1 33 | - qtbase5-dev 34 | -------------------------------------------------------------------------------- /scripts/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | LINK_LIBRARIES( 2 | ZLIB::ZLIB 3 | fmt::fmt-header-only 4 | spdlog::spdlog_header_only) 5 | 6 | ADD_EXECUTABLE(json2bson json2bson.cpp) 7 | 8 | IF(NOT WIN32) 9 | ADD_EXECUTABLE(nbt2json nbt2json.cpp) 10 | ADD_EXECUTABLE(regionReader regionReader.cpp) 11 | ADD_EXECUTABLE(extractChunk extractChunk.cpp) 12 | ADD_EXECUTABLE(chunkPos chunkPos.cpp) 13 | ENDIF() 14 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | This directory contains various scripts to use when debugging and compiling `mcmap`. 4 | 5 | - `json2bson` is used to encode the color file before pasting it in the code; 6 | - `nbt2json` takes a NBT file (as found in `level.dat`) and pastes its output as json; 7 | - `regionReader` reads a region file (`.mca` files) and prints all the chunks present in it; 8 | - `extractChunk` extracts a chunk from a given region file; 9 | 10 | To print a chunk as json, you can pipe those scripts together: 11 | ``` 12 | ./extractChunk X Z | ./nbt2json | python -m json.tool 13 | ``` 14 | 15 | ## Compilation 16 | 17 | Compile using `-DNBT_TOOLS=1` when calling `cmake`. 18 | -------------------------------------------------------------------------------- /scripts/average.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Requires ImageMagick 4 | # Outputs the color average of the image passed as an argument 5 | 6 | set -eo pipefail 7 | 8 | SYSTEM="$(uname)" 9 | EXTRACTDIR=/tmp/extracted_blocks 10 | 11 | macos::install_deps() { 12 | # Script requires imagemagick 13 | if ! command -v convert &>/dev/null; then 14 | if [[ "$SYSTEM" == "Darwin" ]] && command -v brew &>/dev/null; then 15 | brew install imagemagick 16 | fi 17 | fi 18 | } 19 | 20 | macos::mc_home() { 21 | echo $HOME/Library/Application Support/minecraft/versions 22 | } 23 | 24 | linux::mc_home() { 25 | echo $HOME/.minecraft/versions 26 | } 27 | 28 | unpack_assets() { 29 | case "$SYSTEM" in 30 | Darwin) 31 | MC_HOME="$(macos::mc_home)" 32 | ;; 33 | Linux) 34 | MC_HOME="$(linux::mc_home)" 35 | ;; 36 | esac 37 | 38 | if [[ -z "$MINECRAFT_VER" ]]; then 39 | return 40 | fi 41 | 42 | JAR="$MC_HOME/$MINECRAFT_VER/$MINECRAFT_VER.jar" 43 | 44 | if [[ -n "$JAR" ]]; then 45 | mkdir -p "$EXTRACTDIR/$MINECRAFT_VER" 46 | pushd "$EXTRACTDIR/$MINECRAFT_VER" 47 | jar xf "$JAR" assets/minecraft/textures/block 48 | popd 49 | fi 50 | } 51 | 52 | average() { 53 | FILE="$1" 54 | EXTRACTED="$EXTRACTDIR/$MINECRAFT_VER/assets/minecraft/textures/block/$1.png" 55 | if [[ -f "$EXTRACTED" ]] ; then 56 | FILE="$EXTRACTED" 57 | fi 58 | 59 | COLOR="$(convert "$FILE" -resize 1x1 txt:- \ 60 | | grep -o "#[[:xdigit:]]\{6\}" \ 61 | | tr A-F a-f)" 62 | 63 | printf '%s\t%s\n' \ 64 | "$(basename "$FILE")" \ 65 | "$COLOR" 66 | } 67 | 68 | if [[ "$SYSTEM" == Darwin ]] ; then 69 | macos::install_deps 70 | 71 | if [[ ! -d "$EXTRACTDIR/assets/minecraft/textures/" ]] ; then 72 | unpack_assets 73 | fi 74 | fi 75 | 76 | average "$1" 77 | -------------------------------------------------------------------------------- /scripts/chunkPos.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define CHUNK(x) (((x) >> 4) & 0x1f) 4 | #define REGION(x) ((x) >> 9) 5 | 6 | std::string info = "This program's purpose is to locate a chunk in the world. " 7 | "Give it a block coordinate X and Z, and it will output its " 8 | "region file and associated coordinates. This is useful " 9 | "when used in conjunction with the extractChunk program."; 10 | 11 | int main(int argc, char **argv) { 12 | 13 | auto logger = spdlog::stderr_color_mt("chunkPos"); 14 | spdlog::set_default_logger(logger); 15 | 16 | if (argc < 3) { 17 | fmt::print(stderr, "Usage: {} \n{}\n", argv[0], info); 18 | return 1; 19 | } 20 | 21 | int32_t x = atoi(argv[1]), z = atoi(argv[2]); 22 | 23 | int8_t rX = REGION(x), rZ = REGION(z); 24 | uint8_t cX = CHUNK(x), cZ = CHUNK(z); 25 | 26 | fmt::print("Block is in chunk {} {} in r.{}.{}.mca\n", cX, cZ, rX, rZ); 27 | 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /scripts/extractChunk.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::string info = 6 | "This program will extract a single chunk from a region file and pipe it " 7 | "on stdout. X and Z are chunk coordinates (from 0 to 31) inside the given " 8 | "region. The data is raw and not processed in any way. Pipe it into " 9 | "nbt2json for a json representation of the contents of the chunk."; 10 | 11 | #define BUFFERSIZE 2000000 12 | #define DECOMPRESSED_BUFFER BUFFERSIZE 13 | #define REGIONSIZE 32 14 | #define HEADER_SIZE REGIONSIZE *REGIONSIZE * 4 15 | 16 | using std::filesystem::exists; 17 | using std::filesystem::path; 18 | 19 | uint32_t _ntohi(uint8_t *val) { 20 | return (uint32_t(val[0]) << 24) + (uint32_t(val[1]) << 16) + 21 | (uint32_t(val[2]) << 8) + (uint32_t(val[3])); 22 | } 23 | 24 | bool isNumeric(const char *str) { 25 | if (str[0] == '-' && str[1] != '\0') { 26 | ++str; 27 | } 28 | while (*str != '\0') { 29 | if (*str < '0' || *str > '9') { 30 | return false; 31 | } 32 | ++str; 33 | } 34 | return true; 35 | } 36 | 37 | int main(int argc, char **argv) { 38 | uint8_t buffer[BUFFERSIZE], data[DECOMPRESSED_BUFFER]; 39 | size_t length; 40 | FILE *f; 41 | 42 | auto logger = spdlog::stderr_color_mt("extractChunk"); 43 | spdlog::set_default_logger(logger); 44 | 45 | if (argc < 4 || !exists(path(argv[1])) || !isNumeric(argv[2]) || 46 | !isNumeric(argv[3])) { 47 | fmt::print(stderr, "Usage: {} \n{}\n", argv[0], info); 48 | return 1; 49 | } 50 | 51 | uint8_t x = atoi(argv[2]), z = atoi(argv[3]); 52 | 53 | if (x > 31 || z > 31) { 54 | logger::error("Invalid coordinates: {} {} must be 0 and 31", x, z); 55 | return 1; 56 | } 57 | 58 | if (isatty(STDOUT_FILENO)) { 59 | logger::error( 60 | "Not printing compressed data to a terminal, pipe to a file instead"); 61 | return 1; 62 | } 63 | 64 | if (!(f = fopen(argv[1], "r"))) { 65 | logger::error("Error opening file: {}", strerror(errno)); 66 | return 1; 67 | } 68 | 69 | if ((length = fread(buffer, sizeof(uint8_t), HEADER_SIZE, f)) != 70 | HEADER_SIZE) { 71 | logger::error("Error reading header, not enough bytes read."); 72 | fclose(f); 73 | return 1; 74 | } 75 | 76 | const uint32_t offset = 77 | (_ntohi(buffer + (x + z * REGIONSIZE) * 4) >> 8) * 4096; 78 | 79 | if (!offset) { 80 | logger::error("Error: Chunk not found"); 81 | fclose(f); 82 | return 1; 83 | } 84 | 85 | if (0 != fseek(f, offset, SEEK_SET)) { 86 | logger::error("Accessing chunk data in file {} failed: {}", argv[1], 87 | strerror(errno)); 88 | fclose(f); 89 | return 1; 90 | } 91 | 92 | // Read the 5 bytes that give the size and type of data 93 | if (5 != fread(buffer, sizeof(uint8_t), 5, f)) { 94 | logger::error("Reading chunk size from region file {} failed: {}", argv[1], 95 | strerror(errno)); 96 | fclose(f); 97 | return 1; 98 | } 99 | 100 | length = _ntohi(buffer); 101 | length--; // Sometimes the data is 1 byte smaller 102 | 103 | if (fread(buffer, sizeof(uint8_t), length, f) != length) { 104 | logger::error("Not enough data for chunk: {}", strerror(errno)); 105 | fclose(f); 106 | return 1; 107 | } 108 | 109 | fclose(f); 110 | 111 | z_stream zlibStream; 112 | memset(&zlibStream, 0, sizeof(z_stream)); 113 | zlibStream.next_in = (Bytef *)buffer; 114 | zlibStream.next_out = (Bytef *)data; 115 | zlibStream.avail_in = length; 116 | zlibStream.avail_out = DECOMPRESSED_BUFFER; 117 | inflateInit2(&zlibStream, 32 + MAX_WBITS); 118 | 119 | int status = inflate(&zlibStream, Z_FINISH); // decompress in one step 120 | inflateEnd(&zlibStream); 121 | 122 | if (status != Z_STREAM_END) { 123 | logger::error("Decompressing chunk data failed: {}", zError(status)); 124 | return 1; 125 | } 126 | 127 | length = zlibStream.total_out; 128 | 129 | int outfd = dup(STDOUT_FILENO); 130 | close(STDOUT_FILENO); 131 | gzFile out = gzdopen(outfd, "w"); 132 | gzwrite(out, data, length); 133 | gzclose(out); 134 | 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /scripts/json2bson.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::string info = 6 | "This program reads data from the file passed as an argument or stdin on " 7 | "UNIX, parses it as a JSON object and converts it to binary JSON (BSON) on " 8 | "stdout. This script is compiled before mcmap to convert the color file " 9 | "(defined in a JSON) in a format easily embeddable and smaller than a " 10 | "text file."; 11 | 12 | #ifndef _WINDOWS 13 | // If not on windows, allow piping 14 | #include 15 | #endif 16 | 17 | using nlohmann::json; 18 | using std::filesystem::exists; 19 | using std::filesystem::path; 20 | 21 | int main(int argc, char **argv) { 22 | auto logger = spdlog::stderr_color_mt("json2bson"); 23 | spdlog::set_default_logger(logger); 24 | 25 | if (argc > 2) { 26 | fmt::print(stderr, "Usage: {} [json file]\n{}\n", argv[0], info); 27 | 28 | return 1; 29 | } 30 | 31 | json data; 32 | FILE *f; 33 | 34 | #ifndef _WINDOWS 35 | if (argc == 1) 36 | f = fdopen(STDIN_FILENO, "r"); 37 | else 38 | #endif 39 | f = fopen(argv[1], "r"); 40 | 41 | if (!f) { 42 | logger::error("{}: Error opening {}: {}", argv[0], argv[1], 43 | strerror(errno)); 44 | return 1; 45 | } 46 | 47 | try { 48 | data = json::parse(f); 49 | } catch (const json::parse_error &err) { 50 | logger::error("{}: Error parsing {}: {}", argv[0], argv[1], err.what()); 51 | fclose(f); 52 | return 1; 53 | } 54 | fclose(f); 55 | 56 | std::vector bson_vector = json::to_bson(data); 57 | 58 | fmt::print("{{"); 59 | for (auto byte : bson_vector) 60 | fmt::print("{:#x}, ", byte); 61 | fmt::print("}}\n"); 62 | 63 | return 0; 64 | } 65 | -------------------------------------------------------------------------------- /scripts/nbt2json.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | std::string info = 9 | "Convert a NBT file (compressed or not) into a JSON file. This operation " 10 | "is destructive and cannot be reversed. Extremely useful to easily " 11 | "diagnose errors due to format changes."; 12 | 13 | #define BUFFERSIZE 2000000 14 | 15 | using nlohmann::json; 16 | using std::filesystem::exists; 17 | using std::filesystem::path; 18 | 19 | int main(int argc, char **argv) { 20 | auto logger = spdlog::stderr_color_mt("nbt2json"); 21 | spdlog::set_default_logger(logger); 22 | 23 | if (argc > 2 || (argc == 2 && !exists(path(argv[1]))) || 24 | (argc > 2 && !nbt::assert_NBT(argv[1]))) { 25 | fmt::print(stderr, "Usage: {} [NBT file]\n{}\n", argv[0], info); 26 | return 1; 27 | } 28 | 29 | gzFile f; 30 | 31 | #ifndef _WINDOWS 32 | if (argc == 1) 33 | f = gzdopen(STDIN_FILENO, "r"); 34 | else 35 | #endif 36 | f = gzopen(argv[1], "r"); 37 | 38 | if (!f) { 39 | logger::error("Error opening file: {}\n", strerror(errno)); 40 | return 1; 41 | } 42 | 43 | uint8_t buffer[BUFFERSIZE]; 44 | size_t length = gzread(f, buffer, sizeof(uint8_t) * BUFFERSIZE); 45 | gzclose(f); 46 | 47 | nbt::NBT data; 48 | 49 | if (!nbt::parse(buffer, length, data)) { 50 | logger::error("Error parsing data !\n"); 51 | return 1; 52 | } else 53 | fmt::print("{}", json(data).dump()); 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /scripts/regionReader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | std::string info = "This program will output all the information present in a " 9 | "region header from the file passed as an argument."; 10 | 11 | #define BUFFERSIZE 4096 12 | #define REGIONSIZE 32 13 | #define COMPRESSED_BUFFER 500 * 1024 14 | #define DECOMPRESSED_BUFFER 1000 * 1024 15 | #define HEADER_SIZE REGIONSIZE *REGIONSIZE * 4 16 | 17 | using std::filesystem::exists; 18 | using std::filesystem::path; 19 | 20 | uint32_t _ntohi(uint8_t *val) { 21 | return (uint32_t(val[0]) << 24) + (uint32_t(val[1]) << 16) + 22 | (uint32_t(val[2]) << 8) + (uint32_t(val[3])); 23 | } 24 | 25 | bool decompressChunk(FILE *regionHandle, uint8_t *chunkBuffer, uint64_t *length, 26 | const std::filesystem::path &filename) { 27 | uint8_t zData[COMPRESSED_BUFFER]; 28 | 29 | // Read the 5 bytes that give the size and type of data 30 | if (5 != fread(zData, sizeof(uint8_t), 5, regionHandle)) { 31 | logger::debug("Reading chunk size from region file {} failed: {}", 32 | filename.string(), strerror(errno)); 33 | return false; 34 | } 35 | 36 | // Read the size on the first 4 bytes, discard the type 37 | *length = translate(zData); 38 | (*length)--; // Sometimes the data is 1 byte smaller 39 | 40 | if (fread(zData, sizeof(uint8_t), *length, regionHandle) != *length) { 41 | logger::debug("Not enough data for chunk: {}", strerror(errno)); 42 | return false; 43 | } 44 | 45 | z_stream zlibStream; 46 | memset(&zlibStream, 0, sizeof(z_stream)); 47 | zlibStream.next_in = (Bytef *)zData; 48 | zlibStream.next_out = (Bytef *)chunkBuffer; 49 | zlibStream.avail_in = *length; 50 | zlibStream.avail_out = DECOMPRESSED_BUFFER; 51 | inflateInit2(&zlibStream, 32 + MAX_WBITS); 52 | 53 | int status = inflate(&zlibStream, Z_FINISH); // decompress in one step 54 | inflateEnd(&zlibStream); 55 | 56 | if (status != Z_STREAM_END) { 57 | logger::debug("Decompressing chunk data failed: {}", zError(status)); 58 | return false; 59 | } 60 | 61 | *length = zlibStream.total_out; 62 | return true; 63 | } 64 | 65 | int main(int argc, char **argv) { 66 | char time[80]; 67 | uint8_t locations[BUFFERSIZE], timestamps[BUFFERSIZE], 68 | chunkBuffer[DECOMPRESSED_BUFFER]; 69 | uint32_t chunkX, chunkZ, offset; 70 | time_t timestamp; 71 | size_t length; 72 | FILE *f; 73 | struct tm saved; 74 | 75 | auto logger = spdlog::stderr_color_mt("regionReader"); 76 | spdlog::set_default_logger(logger); 77 | spdlog::set_level(spdlog::level::info); 78 | 79 | if (argc < 2 || !exists(path(argv[1]))) { 80 | fmt::print("Usage: {} \n{}\n", argv[0], info); 81 | return 1; 82 | } 83 | 84 | if (!(f = fopen(argv[1], "r"))) { 85 | logger::error("Error opening file: {}", strerror(errno)); 86 | return 1; 87 | } 88 | 89 | if ((length = fread(locations, sizeof(uint8_t), HEADER_SIZE, f)) != 90 | HEADER_SIZE) { 91 | logger::error("Error reading header, not enough bytes read."); 92 | fclose(f); 93 | return 1; 94 | } 95 | 96 | if ((length = fread(timestamps, sizeof(uint8_t), HEADER_SIZE, f)) != 97 | HEADER_SIZE) { 98 | logger::error("Error reading header, not enough bytes read."); 99 | fclose(f); 100 | return 1; 101 | } 102 | 103 | fmt::print("{}\t{}\t{}\t{}\t{}\n", "X", "Z", "Last Saved", "DataVersion", 104 | "Status"); 105 | 106 | for (int it = 0; it < REGIONSIZE * REGIONSIZE; it++) { 107 | // Bound check 108 | chunkX = it & 0x1f; 109 | chunkZ = it >> 5; 110 | 111 | // Get the location of the data from the header 112 | offset = (_ntohi(locations + it * 4) >> 8); 113 | timestamp = _ntohi(timestamps + it * 4); 114 | 115 | auto data_version = 0; 116 | std::string status = "unknown"; 117 | 118 | if (offset) { 119 | nbt::NBT nbt_data; 120 | 121 | fseek(f, offset * 4096, SEEK_SET); 122 | if (decompressChunk(f, chunkBuffer, &length, argv[1])) { 123 | if (nbt::parse(chunkBuffer, length, nbt_data)) { 124 | if (nbt_data.contains("DataVersion")) { 125 | data_version = nbt_data["DataVersion"].get(); 126 | } 127 | if (nbt_data.contains("Status")) { 128 | status = nbt_data["Status"].get(); 129 | } 130 | } 131 | } 132 | 133 | saved = *localtime(×tamp); 134 | strftime(time, 80, "%c", &saved); 135 | } else { 136 | strcpy(time, "No data for chunk"); 137 | } 138 | 139 | fmt::print("{}\t{}\t{}\t{}\t{}\n", chunkX, chunkZ, std::string(time), 140 | data_version, status); 141 | } 142 | 143 | fclose(f); 144 | return 0; 145 | } 146 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB JSON colors.json) 2 | SET(BSON ${CMAKE_CURRENT_SOURCE_DIR}/colors.bson CACHE FILEPATH "Embedded color file, generated from colors.json") 3 | 4 | SET(ADDITIONAL_CLEAN_FILES ${ADDITIONAL_CLEAN_FILES} ${BSON}) 5 | 6 | ADD_CUSTOM_COMMAND(OUTPUT ${BSON} 7 | COMMAND json2bson ${JSON} > ${BSON} 8 | DEPENDS ${JSON}) 9 | 10 | SET(SOURCES ${SOURCES} 11 | ${BSON} 12 | blocktypes.def 13 | block_drawers.cpp 14 | canvas.cpp 15 | chunk.cpp 16 | colors.cpp 17 | helper.cpp 18 | mcmap.cpp 19 | png.cpp 20 | savefile.cpp 21 | section.cpp 22 | settings.cpp 23 | worldloader.cpp 24 | chunk_format_versions/assert.cpp 25 | chunk_format_versions/get_section.cpp 26 | chunk_format_versions/section_format.cpp 27 | VERSION) 28 | 29 | ADD_LIBRARY(mcmap_core STATIC ${SOURCES}) 30 | TARGET_LINK_LIBRARIES( 31 | mcmap_core 32 | ZLIB::ZLIB 33 | PNG::PNG 34 | fmt::fmt-header-only 35 | spdlog::spdlog_header_only) 36 | 37 | IF (Git_FOUND) 38 | SET_SOURCE_FILES_PROPERTIES( 39 | mcmap.cpp 40 | PROPERTIES COMPILE_DEFINITIONS 41 | SCM_COMMIT="${GIT_DESCRIBE}") 42 | ENDIF() 43 | 44 | IF (OpenMP_FOUND) 45 | TARGET_LINK_LIBRARIES( 46 | mcmap_core 47 | OpenMP::OpenMP_CXX) 48 | ENDIF() 49 | 50 | ADD_SUBDIRECTORY(cli) 51 | ADD_SUBDIRECTORY(graphical) 52 | ADD_SUBDIRECTORY(include) 53 | -------------------------------------------------------------------------------- /src/VERSION: -------------------------------------------------------------------------------- 1 | #define VERSION "mcmap 3.0.4" 2 | #define COMMENT "compatible with Minecraft v1.13+" 3 | -------------------------------------------------------------------------------- /src/block_drawers.h: -------------------------------------------------------------------------------- 1 | #ifndef BLOCK_DRAWERS_H_ 2 | #define BLOCK_DRAWERS_H_ 3 | 4 | #include "./canvas.h" 5 | #include "./colors.h" 6 | #include "./nbt/nbt.hpp" 7 | 8 | // This obscure typedef allows to create a member function pointer array 9 | // (ouch) to render different block types without a switch case 10 | typedef void (*drawer)(IsometricCanvas *, const uint32_t, const uint32_t, 11 | const nbt::NBT &, const Colors::Block *); 12 | 13 | // The default block type, hardcoded 14 | void drawFull(IsometricCanvas *, const uint32_t, const uint32_t, 15 | const nbt::NBT &, const Colors::Block *); 16 | 17 | // The other block types are loaded at compile-time from the `blocktypes.def` 18 | // file, with some macro manipulation 19 | #define DEFINETYPE(STRING, CALLBACK) \ 20 | void CALLBACK(IsometricCanvas *, const uint32_t, const uint32_t, \ 21 | const nbt::NBT &, const Colors::Block *); 22 | #include "./blocktypes.def" 23 | #undef DEFINETYPE 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/blocktypes.def: -------------------------------------------------------------------------------- 1 | /* Definition of the supported block types. 2 | * This file defines the specific block types, the full block type 3 | * being enabled by default. The left argument is the identifier in the `type` 4 | * attribute of the `colors.json` file, the other being the callback to use in 5 | * the code. 6 | * They are imported at compile time with some macro magic */ 7 | 8 | DEFINETYPE("Hide", drawHidden) // Non-renderable (levers etc) 9 | DEFINETYPE("Head", drawHead) // Small block, for heads but also sea-pickles, etc 10 | DEFINETYPE("Clear", drawTransparent) // See-through: clearer look when in large quantities. 11 | DEFINETYPE("Slab", drawSlab) // Slab and half-blocks 12 | DEFINETYPE("Stair", drawStair) // Stairs, rendered depending on orientation 13 | DEFINETYPE("Torch", drawTorch) // Torch/end rod. Accent is the color of the flame. 14 | DEFINETYPE("Rod", drawRod) // Fence and walls. 15 | DEFINETYPE("Ore", drawOre) // Ores 16 | DEFINETYPE("Wire", drawWire) // Redstone dust and tripwire 17 | DEFINETYPE("Plant", drawPlant) // Flower and plants. 18 | DEFINETYPE("UnderwaterPlant", drawUnderwaterPlant) // Like Plant, but air is water 19 | DEFINETYPE("Thin", drawThin) // Snow-like. 20 | DEFINETYPE("Grown", drawGrown) // Grass-like blocks, accent is the color on top. 21 | DEFINETYPE("Fire", drawFire) // Fire-like. 22 | DEFINETYPE("Log", drawLog) // Axis-oriented block, accent is the color on top and bottom. 23 | DEFINETYPE("Lamp", drawLamp) // Blocks that have a `lit` property. Accent is lit color. 24 | DEFINETYPE("Beam", drawBeam) // Element of beacon beams and markers 25 | -------------------------------------------------------------------------------- /src/canvas.h: -------------------------------------------------------------------------------- 1 | #ifndef CANVAS_H_ 2 | #define CANVAS_H_ 3 | 4 | #include "./helper.h" 5 | #include "./png.h" 6 | #include "./section.h" 7 | #include "./worldloader.h" 8 | #include 9 | #include 10 | #include 11 | 12 | #define CHANSPERPIXEL 4 13 | #define BYTESPERCHAN 1 14 | #define BYTESPERPIXEL 4 15 | 16 | #define BLOCKHEIGHT 3 17 | 18 | struct Beam { 19 | uint8_t position; 20 | const Colors::Block *color; 21 | 22 | Beam() : position(0), color(nullptr){}; 23 | Beam(uint8_t x, uint8_t z, const Colors::Block *c) 24 | : position((x << 4) + z), color(c){}; 25 | 26 | inline uint8_t x() const { return position >> 4; } 27 | inline uint8_t z() const { return position & 0x0f; } 28 | 29 | inline bool column(uint8_t x, uint8_t z) const { 30 | return position == ((x << 4) + z); 31 | } 32 | 33 | Beam &operator=(Beam &&other) { 34 | position = other.position; 35 | color = other.color; 36 | return *this; 37 | } 38 | }; 39 | 40 | // Canvas 41 | // Common features of all canvas types. 42 | struct Canvas { 43 | enum BufferType { BYTES, CANVAS, IMAGE, EMPTY }; 44 | 45 | World::Coordinates map; // The coordinates describing the 3D map 46 | 47 | inline size_t width() const { 48 | if (type != EMPTY && !map.isUndefined()) 49 | return (map.sizeX() + map.sizeZ()) * 2; 50 | return 0; 51 | } 52 | 53 | inline size_t height() const { 54 | if (type != EMPTY && !map.isUndefined()) 55 | return map.sizeX() + map.sizeZ() + 56 | (map.maxY - map.minY + 1) * BLOCKHEIGHT - 1; 57 | return 0; 58 | } 59 | 60 | virtual size_t getLine(uint8_t *buffer, size_t size, uint64_t line) const { 61 | switch (type) { 62 | case BYTES: 63 | return _get_line(&drawing.bytes_buffer->operator[](0), buffer, size, 64 | line); 65 | 66 | case CANVAS: 67 | return _get_line(*drawing.canvas_buffer, buffer, size, line); 68 | 69 | case IMAGE: 70 | return _get_line(drawing.image_buffer, buffer, size, line); 71 | 72 | default: 73 | return 0; 74 | } 75 | } 76 | 77 | size_t _get_line(const uint8_t *, uint8_t *, size_t, uint64_t) const; 78 | size_t _get_line(PNG::PNGReader *, uint8_t *, size_t, uint64_t) const; 79 | size_t _get_line(const std::vector &, uint8_t *, size_t, 80 | uint64_t) const; 81 | 82 | bool save(const std::filesystem::path, uint8_t = 0, 83 | Progress::Callback = Progress::Status::quiet) const; 84 | bool tile(const std::filesystem::path, uint16_t tilesize, 85 | Progress::Callback = Progress::Status::quiet) const; 86 | 87 | virtual std::string to_string() const; 88 | 89 | union DrawingBuffer { 90 | long null_buffer; 91 | std::vector *bytes_buffer; 92 | std::vector *canvas_buffer; 93 | PNG::PNGReader *image_buffer; 94 | 95 | DrawingBuffer() : null_buffer(0) {} 96 | 97 | DrawingBuffer(std::filesystem::path file) { 98 | image_buffer = new PNG::PNGReader(file); 99 | } 100 | 101 | DrawingBuffer(std::vector &&fragments) { 102 | canvas_buffer = new std::vector(std::move(fragments)); 103 | } 104 | 105 | DrawingBuffer(BufferType type) { 106 | switch (type) { 107 | case BYTES: { 108 | bytes_buffer = new std::vector(); 109 | break; 110 | } 111 | 112 | case CANVAS: { 113 | canvas_buffer = new std::vector(); 114 | break; 115 | } 116 | 117 | case IMAGE: 118 | logger::error("Default constructing image canvas not supported"); 119 | break; 120 | 121 | default: { 122 | null_buffer = long(0); 123 | } 124 | } 125 | } 126 | 127 | void destroy(BufferType type) { 128 | switch (type) { 129 | case BYTES: { 130 | if (bytes_buffer) 131 | delete bytes_buffer; 132 | break; 133 | } 134 | 135 | case CANVAS: { 136 | if (canvas_buffer) 137 | delete canvas_buffer; 138 | break; 139 | } 140 | 141 | case IMAGE: { 142 | if (image_buffer) 143 | delete image_buffer; 144 | break; 145 | } 146 | 147 | default: 148 | break; 149 | } 150 | } 151 | }; 152 | 153 | BufferType type; 154 | DrawingBuffer drawing; 155 | 156 | Canvas() : type(EMPTY), drawing() { map.setUndefined(); } 157 | 158 | Canvas(BufferType _type) : type(_type), drawing(_type) { map.setUndefined(); } 159 | 160 | Canvas(std::vector &&fragments) : drawing(std::move(fragments)) { 161 | map.setUndefined(); 162 | type = CANVAS; 163 | 164 | // Determine the size of the virtual map 165 | // All the maps are oriented as NW to simplify the process 166 | for (auto &fragment : *drawing.canvas_buffer) { 167 | World::Coordinates oriented = fragment.map.orient(Map::NW); 168 | map += oriented; 169 | } 170 | } 171 | 172 | Canvas(const World::Coordinates &map, const fs::path &file) 173 | : map(map), type(IMAGE), drawing(file) { 174 | assert(width() == drawing.image_buffer->get_width()); 175 | assert(height() == drawing.image_buffer->get_height()); 176 | }; 177 | 178 | Canvas(Canvas &&other) { *this = std::move(other); } 179 | 180 | Canvas &operator=(Canvas &&other) { 181 | map = other.map; 182 | 183 | type = other.type; 184 | switch (type) { 185 | case BYTES: { 186 | drawing.bytes_buffer = std::move(other.drawing.bytes_buffer); 187 | other.drawing.bytes_buffer = nullptr; 188 | break; 189 | } 190 | 191 | case CANVAS: { 192 | drawing.canvas_buffer = std::move(other.drawing.canvas_buffer); 193 | other.drawing.canvas_buffer = nullptr; 194 | break; 195 | } 196 | 197 | case IMAGE: { 198 | drawing.image_buffer = std::move(other.drawing.image_buffer); 199 | other.drawing.image_buffer = nullptr; 200 | break; 201 | } 202 | 203 | default: 204 | drawing.null_buffer = long(0); 205 | } 206 | 207 | return *this; 208 | } 209 | 210 | ~Canvas() { drawing.destroy(type); } 211 | }; 212 | 213 | struct ImageCanvas : Canvas { 214 | const std::filesystem::path file; 215 | 216 | ImageCanvas(const World::Coordinates &map, const fs::path &file) 217 | : Canvas(map, file), file(file) {} 218 | }; 219 | 220 | // Isometric canvas 221 | // This structure holds the final bitmap data, a 2D array of pixels. It is 222 | // created with a set of 3D coordinates, and translate every block drawn 223 | // into a 2D position. 224 | struct IsometricCanvas : Canvas { 225 | using Chunk = mcmap::Chunk; 226 | using marker_array_t = std::array; 227 | 228 | bool shading, lighting, beamColumn; 229 | size_t rendered; 230 | 231 | size_t width, height; 232 | 233 | uint32_t sizeX, sizeZ; // The size of the 3D map 234 | uint8_t offsetX, offsetZ; // Offset of the first block in the first chunk 235 | 236 | Colors::Palette palette; // The colors to use when drawing 237 | Colors::Block air, 238 | water, // fire, earth. Teh four nations lived in harmoiny 239 | beaconBeam; // Cached colors for easy access 240 | 241 | // TODO bye bye 242 | uint8_t totalMarkers = 0; 243 | marker_array_t markers; 244 | 245 | std::array brightnessLookup; 246 | 247 | Chunk::section_array_t::const_iterator current_section, last_section, 248 | left_section, right_section; 249 | 250 | // In-chunk variables 251 | uint32_t chunkX; 252 | uint32_t chunkZ; 253 | 254 | // Beams in the chunk being rendered 255 | uint8_t beamNo = 0; 256 | Beam beams[256]; 257 | 258 | uint8_t orientedX, orientedZ, y; 259 | 260 | // Section array with an empty section to return when a section is not 261 | // available 262 | Chunk::section_array_t empty_section; 263 | 264 | IsometricCanvas() : Canvas(BYTES), rendered(0) { empty_section.resize(1); } 265 | 266 | inline bool empty() const { return !rendered; } 267 | 268 | void setColors(const Colors::Palette &); 269 | void setMap(const World::Coordinates &); 270 | void setMarkers(uint8_t n, const marker_array_t array) { 271 | totalMarkers = n; 272 | markers = array; 273 | } 274 | 275 | // Drawing methods 276 | // Helpers for position lookup 277 | void orientChunk(int32_t &x, int32_t &z); 278 | void orientSection(uint8_t &x, uint8_t &z); 279 | inline uint8_t *pixel(uint32_t x, uint32_t y) { 280 | return &(*drawing.bytes_buffer)[(x + y * width) * BYTESPERPIXEL]; 281 | } 282 | 283 | // Drawing entrypoints 284 | void renderTerrain(Terrain::Data &); 285 | void renderChunk(Terrain::Data &); 286 | void renderSection(const Section &); 287 | // Draw a block from virtual coords in the canvas 288 | void renderBlock(const Colors::Block *, const uint32_t, const uint32_t, 289 | const int32_t, const nbt::NBT &); 290 | 291 | // Empty section with only beams 292 | void renderBeamSection(const int64_t, const int64_t, const uint8_t); 293 | 294 | const Colors::Block *nextBlock(); 295 | Chunk::section_array_t::const_iterator section_up(); 296 | Chunk::section_array_t::const_iterator section_left(const Terrain::Data &); 297 | Chunk::section_array_t::const_iterator section_right(const Terrain::Data &); 298 | }; 299 | 300 | struct CompositeCanvas : public Canvas { 301 | // A sparse canvas made with smaller canvasses 302 | // 303 | // To render multiple canvasses made by threads, we compose an image from 304 | // them directly. This object allows to do so. It is given a list of 305 | // canvasses, and can be read as an image (made out of lines, with a 306 | // height and width) that is composed of the canvasses, without actually 307 | // using any more memory. 308 | // 309 | // This is done by keeping track of the offset of each sub-canvas from the 310 | // top left of the image. When reading a line, it is composed of the lines 311 | // of each sub-canvas, with the appropriate offset. 312 | // 313 | // +-------------------+ 314 | // |Composite Canvas | 315 | // |+------------+ | 316 | // ||Canvas 1 | | 317 | // || +------------+| 318 | // || |Canvas 2 || 319 | // ||====|============|| < Read line 320 | // || | || 321 | // |+----| || 322 | // | | || 323 | // | +------------+| 324 | // +-------------------+ 325 | 326 | CompositeCanvas(std::vector &&); 327 | 328 | bool empty() const; 329 | }; 330 | 331 | #endif 332 | -------------------------------------------------------------------------------- /src/chunk.cpp: -------------------------------------------------------------------------------- 1 | #include "./chunk.h" 2 | #include "./chunk_format_versions/assert.hpp" 3 | #include "./chunk_format_versions/get_section.hpp" 4 | #include 5 | #include 6 | 7 | namespace mcmap { 8 | 9 | namespace versions { 10 | std::map> assert = { 11 | {3458, assert_versions::v3458}, {2844, assert_versions::v2844}, 12 | {1976, assert_versions::v1976}, {1628, assert_versions::v1628}, 13 | {0, assert_versions::catchall}, 14 | }; 15 | 16 | std::map> sections = { 17 | {2844, sections_versions::v2844}, 18 | {1628, sections_versions::v1628}, 19 | {0, sections_versions::catchall}, 20 | }; 21 | 22 | } // namespace versions 23 | 24 | Chunk::Chunk() : data_version(-1) {} 25 | 26 | Chunk::Chunk(const nbt::NBT &data, const Colors::Palette &palette, 27 | const coordinates pos) 28 | : Chunk() { 29 | position = pos; 30 | 31 | // If there is nothing to render 32 | if (data.is_end() || !assert_chunk(data)) 33 | return; 34 | 35 | // This value is primordial: it states which version of minecraft the chunk 36 | // was created under, and we use it to know which interpreter to use later 37 | // in the sections 38 | data_version = data["DataVersion"].get(); 39 | 40 | nbt::NBT sections_list; 41 | 42 | auto sections_it = compatible(versions::sections, data_version); 43 | 44 | if (sections_it != versions::sections.end()) { 45 | sections_list = sections_it->second(data); 46 | 47 | for (const auto &raw_section : sections_list) { 48 | Section section(raw_section, data_version, this->position); 49 | section.loadPalette(palette); 50 | sections.push_back(std::move(section)); 51 | } 52 | } 53 | } 54 | 55 | Chunk::Chunk(Chunk &&other) { *this = std::move(other); } 56 | 57 | Chunk &Chunk::operator=(Chunk &&other) { 58 | position = other.position; 59 | data_version = other.data_version; 60 | sections = std::move(other.sections); 61 | 62 | return *this; 63 | } 64 | 65 | bool Chunk::assert_chunk(const nbt::NBT &chunk) { 66 | if (chunk.is_end() // Catch uninitialized chunks 67 | || !chunk.contains("DataVersion")) // Dataversion is required 68 | { 69 | logger::trace("Chunk is empty or invalid"); 70 | return false; 71 | } 72 | 73 | const int version = chunk["DataVersion"].get(); 74 | 75 | auto assert_it = compatible(versions::assert, version); 76 | 77 | if (assert_it == versions::assert.end()) { 78 | logger::trace("Unsupported chunk version: {}", version); 79 | return false; 80 | } 81 | 82 | return assert_it->second(chunk); 83 | } 84 | 85 | } // namespace mcmap 86 | 87 | mcmap::Chunk::coordinates left_in(Map::Orientation o) { 88 | switch (o) { 89 | case Map::NW: 90 | return {0, 1}; 91 | case Map::SW: 92 | return {1, 0}; 93 | case Map::SE: 94 | return {0, -1}; 95 | case Map::NE: 96 | return {-1, 0}; 97 | } 98 | 99 | return {0, 0}; 100 | } 101 | 102 | mcmap::Chunk::coordinates right_in(Map::Orientation o) { 103 | switch (o) { 104 | case Map::NW: 105 | return {1, 0}; 106 | case Map::SW: 107 | return {0, -1}; 108 | case Map::SE: 109 | return {-1, 0}; 110 | case Map::NE: 111 | return {0, 1}; 112 | } 113 | 114 | return {0, 0}; 115 | } 116 | -------------------------------------------------------------------------------- /src/chunk.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./section.h" 4 | #include <2DCoordinates.hpp> 5 | #include 6 | #include 7 | 8 | namespace mcmap { 9 | 10 | struct Chunk { 11 | using nbt_t = nbt::NBT; 12 | using version_t = int32_t; 13 | using section_t = Section; 14 | using section_array_t = std::vector; 15 | using coordinates = Coordinates; 16 | 17 | coordinates position; 18 | version_t data_version; 19 | section_array_t sections; 20 | 21 | Chunk(); 22 | Chunk(const nbt_t &, const Colors::Palette &, const coordinates); 23 | Chunk(Chunk &&); 24 | 25 | Chunk &operator=(Chunk &&); 26 | 27 | bool valid() const { return data_version != -1; } 28 | 29 | static bool assert_chunk(const nbt_t &); 30 | }; 31 | 32 | } // namespace mcmap 33 | 34 | mcmap::Chunk::coordinates left_in(Map::Orientation); 35 | mcmap::Chunk::coordinates right_in(Map::Orientation); 36 | -------------------------------------------------------------------------------- /src/chunk_format_versions/assert.cpp: -------------------------------------------------------------------------------- 1 | #include "./assert.hpp" 2 | 3 | namespace mcmap { 4 | namespace versions { 5 | namespace assert_versions { 6 | bool v3458(const nbt::NBT &chunk) { 7 | // Minecraft 1.20-pre5, randomly changing things 8 | return chunk.contains("sections") // No sections mean no blocks 9 | && chunk.contains("Status") // Ensure the status is `minecraft:full` 10 | && chunk["Status"].get() == "minecraft:full"; 11 | } 12 | 13 | bool v2844(const nbt::NBT &chunk) { 14 | // Snapshot 21w43a 15 | return chunk.contains("sections") // No sections mean no blocks 16 | && chunk.contains("Status") // Ensure the status is `full` 17 | && chunk["Status"].get() == "full"; 18 | } 19 | 20 | bool v1976(const nbt::NBT &chunk) { 21 | // From 1.14 onwards 22 | return chunk.contains("Level") // Level data is required 23 | && chunk["Level"].contains("Sections") // No sections mean no blocks 24 | && chunk["Level"].contains("Status") // Ensure the status is `full` 25 | && chunk["Level"]["Status"].get() == "full"; 26 | } 27 | 28 | bool v1628(const nbt::NBT &chunk) { 29 | // From 1.13 onwards 30 | return chunk.contains("Level") // Level data is required 31 | && chunk["Level"].contains("Sections") // No sections mean no blocks 32 | && chunk["Level"].contains("Status") // Ensure the status is `full` 33 | && chunk["Level"]["Status"].get() == 34 | "postprocessed"; 35 | } 36 | 37 | bool catchall(const nbt::NBT &chunk) { 38 | logger::trace("Unsupported DataVersion: {}", chunk["DataVersion"].get()); 39 | return false; 40 | } 41 | } // namespace assert_versions 42 | } // namespace versions 43 | } // namespace mcmap 44 | -------------------------------------------------------------------------------- /src/chunk_format_versions/assert.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace mcmap { 4 | namespace versions { 5 | namespace assert_versions { 6 | bool v3458(const nbt::NBT &chunk); 7 | 8 | bool v2844(const nbt::NBT &chunk); 9 | 10 | bool v1976(const nbt::NBT &chunk); 11 | 12 | bool v1628(const nbt::NBT &chunk); 13 | 14 | bool catchall(const nbt::NBT &chunk); 15 | } // namespace assert_versions 16 | } // namespace versions 17 | } // namespace mcmap 18 | -------------------------------------------------------------------------------- /src/chunk_format_versions/get_section.cpp: -------------------------------------------------------------------------------- 1 | #include "get_section.hpp" 2 | 3 | namespace mcmap { 4 | namespace versions { 5 | namespace sections_versions { 6 | nbt::NBT v2844(const nbt::NBT &chunk) { return chunk["sections"]; } 7 | nbt::NBT v1628(const nbt::NBT &chunk) { return chunk["Level"]["Sections"]; } 8 | nbt::NBT catchall(const nbt::NBT &chunk) { 9 | logger::trace("Unsupported DataVersion: {}", chunk["DataVersion"].get()); 10 | return nbt::NBT(std::vector()); 11 | } 12 | } // namespace sections_versions 13 | } // namespace versions 14 | } // namespace mcmap 15 | -------------------------------------------------------------------------------- /src/chunk_format_versions/get_section.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace mcmap { 4 | namespace versions { 5 | namespace sections_versions { 6 | nbt::NBT v2844(const nbt::NBT &); 7 | nbt::NBT v1628(const nbt::NBT &); 8 | nbt::NBT catchall(const nbt::NBT &); 9 | } // namespace sections_versions 10 | } // namespace versions 11 | } // namespace mcmap 12 | -------------------------------------------------------------------------------- /src/chunk_format_versions/section_format.cpp: -------------------------------------------------------------------------------- 1 | #include "section_format.hpp" 2 | 3 | namespace mcmap { 4 | namespace versions { 5 | namespace block_states_versions { 6 | void post116(const uint8_t index_length, 7 | const std::vector *blockStates, 8 | Section::block_array &buffer) { 9 | // NEW in 1.16, longs are padded by 0s when a block cannot fit, so no more 10 | // overflow to deal with ! 11 | 12 | for (uint16_t index = 0; index < 4096; index++) { 13 | // Determine how many indexes each long holds 14 | const uint8_t blocksPerLong = 64 / index_length; 15 | 16 | // Calculate where in the long array is the long containing the right index. 17 | const uint16_t longIndex = index / blocksPerLong; 18 | 19 | // Once we located a long, we have to know where in the 64 bits 20 | // the relevant block is located. 21 | const uint8_t padding = (index - longIndex * blocksPerLong) * index_length; 22 | 23 | // Bring the data to the first bits of the long, then extract it by bitwise 24 | // comparison 25 | const uint16_t blockIndex = ((*blockStates)[longIndex] >> padding) & 26 | ((uint64_t(1) << index_length) - 1); 27 | 28 | buffer[index] = blockIndex; 29 | } 30 | } 31 | 32 | void pre116(const uint8_t index_length, const std::vector *blockStates, 33 | Section::block_array &buffer) { 34 | // The `BlockStates` array contains data on the section's blocks. You have to 35 | // extract it by understanfing its structure. 36 | // 37 | // Although it is a array of long values, one must see it as an array of block 38 | // indexes, whose element size depends on the size of the Palette. This 39 | // routine locates the necessary long, extracts the block with bit 40 | // comparisons. 41 | // 42 | // The length of a block index has to be coded on the minimal possible size, 43 | // which is the logarithm in base2 of the size of the palette, or 4 if the 44 | // logarithm is smaller. 45 | 46 | for (uint16_t index = 0; index < 4096; index++) { 47 | 48 | // We skip the `position` first blocks, of length `size`, then divide by 64 49 | // to get the number of longs to skip from the array 50 | const uint16_t skip_longs = (index * index_length) >> 6; 51 | 52 | // Once we located the data in a long, we have to know where in the 64 bits 53 | // it is located. This is the remaining of the previous operation 54 | const int8_t padding = (index * index_length) & 63; 55 | 56 | // Sometimes the data of an index does not fit entirely into a long, so we 57 | // check if there is overflow 58 | const int8_t overflow = 59 | (padding + index_length > 64 ? padding + index_length - 64 : 0); 60 | 61 | // This complicated expression extracts the necessary bits from the current 62 | // long. 63 | // 64 | // Lets say we need the following bits in a long (not to scale): 65 | // 10011100111001110011100 66 | // ^^^^^ 67 | // We do this by shifting (>>) the data by padding, to get the relevant bits 68 | // on the end of the long: 69 | // ???????????????10011100 70 | // ^^^^^ 71 | // We then apply a mask to get only the relevant bits: 72 | // ???????????????10011100 73 | // 00000000000000000011111 & 74 | // 00000000000000000011100 <- result 75 | // 76 | // The mask is made at the size of the data, using the formula (1 << n) - 1, 77 | // the resulting bitset is of the following shape: 0...01...1 with n 1s. 78 | // 79 | // If there is an overflow, the mask size is reduced, as not to catch noise 80 | // from the padding (ie the interrogation points earlier) that appear on 81 | // ARM32. 82 | uint16_t lower_data = ((*blockStates)[skip_longs] >> padding) & 83 | ((uint64_t(1) << (index_length - overflow)) - 1); 84 | 85 | if (overflow > 0) { 86 | // The exact same process is used to catch the overflow from the next long 87 | const uint16_t upper_data = 88 | ((*blockStates)[skip_longs + 1]) & ((uint64_t(1) << overflow) - 1); 89 | // We then associate both values to create the final value 90 | lower_data = lower_data | (upper_data << (index_length - overflow)); 91 | } 92 | 93 | // lower_data now contains the index in the palette 94 | buffer[index] = lower_data; 95 | } 96 | } 97 | } // namespace block_states_versions 98 | 99 | namespace init_versions { 100 | void v1628(Section *target, const nbt::NBT &raw_section) { 101 | if (raw_section.contains("BlockStates") && raw_section.contains("Palette")) { 102 | target->palette = 103 | *raw_section["Palette"].get *>(); 104 | const nbt::NBT::tag_long_array_t *blockStates = 105 | raw_section["BlockStates"].get(); 106 | 107 | // Remove the air that is default-constructed 108 | target->colors.clear(); 109 | // Anticipate the color input from the palette's size 110 | target->colors.reserve(target->palette.size()); 111 | 112 | // The length in bits of a block is the log2 of the palette's size or 4, 113 | // whichever is greatest. Ranges from 4 to 12. 114 | const uint8_t blockBitLength = 115 | std::max(uint8_t(ceil(log2(target->palette.size()))), uint8_t(4)); 116 | 117 | // Parse the blockstates for block info 118 | block_states_versions::pre116(blockBitLength, blockStates, target->blocks); 119 | } else 120 | logger::trace("Section {} does not contain BlockStates or Palette !", 121 | target->Y); 122 | } 123 | 124 | void v2534(Section *target, const nbt::NBT &raw_section) { 125 | if (raw_section.contains("BlockStates") && raw_section.contains("Palette")) { 126 | target->palette = 127 | *raw_section["Palette"].get *>(); 128 | const nbt::NBT::tag_long_array_t *blockStates = 129 | raw_section["BlockStates"].get(); 130 | 131 | // Remove the air that is default-constructed 132 | target->colors.clear(); 133 | // Anticipate the color input from the palette's size 134 | target->colors.reserve(target->palette.size()); 135 | 136 | // The length in bits of a block is the log2 of the palette's size or 4, 137 | // whichever is greatest. Ranges from 4 to 12. 138 | const uint8_t blockBitLength = 139 | std::max(uint8_t(ceil(log2(target->palette.size()))), uint8_t(4)); 140 | 141 | // Parse the blockstates for block info 142 | block_states_versions::post116(blockBitLength, blockStates, target->blocks); 143 | } else 144 | logger::trace("Section {} does not contain BlockStates or Palette !", 145 | target->Y); 146 | } 147 | 148 | void v2840(Section *target, const nbt::NBT &raw_section) { 149 | if (raw_section.contains("block_states") && 150 | raw_section["block_states"].contains("data") && 151 | raw_section["block_states"].contains("palette")) { 152 | target->palette = *raw_section["block_states"]["palette"] 153 | .get *>(); 154 | const nbt::NBT::tag_long_array_t *blockStates = 155 | raw_section["block_states"]["data"] 156 | .get(); 157 | 158 | // Remove the air that is default-constructed 159 | target->colors.clear(); 160 | // Anticipate the color input from the palette's size 161 | target->colors.reserve(target->palette.size()); 162 | 163 | // The length in bits of a block is the log2 of the palette's size or 4, 164 | // whichever is greatest. Ranges from 4 to 12. 165 | const uint8_t blockBitLength = 166 | std::max(uint8_t(ceil(log2(target->palette.size()))), uint8_t(4)); 167 | 168 | // Parse the blockstates for block info 169 | block_states_versions::post116(blockBitLength, blockStates, target->blocks); 170 | } else 171 | logger::trace("Section {} does not contain BlockStates or Palette !", 172 | target->Y); 173 | } 174 | 175 | void v3100(Section *target, const nbt::NBT &raw_section) { 176 | // NEW in 1.19, some sections can omit the block_states array when only one 177 | // block is present in the palette to signify that the whole section is 178 | // filled with one block, so this checks for that special case 179 | 180 | if (raw_section.contains("block_states") && 181 | raw_section["block_states"].contains("palette")) { 182 | target->palette = *raw_section["block_states"]["palette"] 183 | .get *>(); 184 | // Remove the air that is default-constructed 185 | target->colors.clear(); 186 | // Anticipate the color input from the palette's size 187 | target->colors.reserve(target->palette.size()); 188 | 189 | if (raw_section["block_states"].contains("data")) { 190 | const nbt::NBT::tag_long_array_t *blockStates = 191 | raw_section["block_states"]["data"] 192 | .get(); 193 | 194 | // The length in bits of a block is the log2 of the palette's size or 4, 195 | // whichever is greatest. Ranges from 4 to 12. 196 | const uint8_t blockBitLength = 197 | std::max(uint8_t(ceil(log2(target->palette.size()))), uint8_t(4)); 198 | 199 | // Parse the blockstates for block info 200 | block_states_versions::post116(blockBitLength, blockStates, 201 | target->blocks); 202 | } else { 203 | target->blocks.fill(0); 204 | } 205 | } else 206 | logger::trace("Section {} does not contain a palette, aborting", target->Y); 207 | } 208 | 209 | void catchall(Section *, const nbt::NBT &) { 210 | logger::trace("Unsupported DataVersion"); 211 | } 212 | } // namespace init_versions 213 | } // namespace versions 214 | } // namespace mcmap 215 | -------------------------------------------------------------------------------- /src/chunk_format_versions/section_format.hpp: -------------------------------------------------------------------------------- 1 | #include "../section.h" 2 | #include 3 | 4 | namespace mcmap { 5 | namespace versions { 6 | namespace block_states_versions { 7 | void post116(const uint8_t, const std::vector *, 8 | Section::block_array &); 9 | 10 | void pre116(const uint8_t, const std::vector *, 11 | Section::block_array &); 12 | } // namespace block_states_versions 13 | 14 | namespace init_versions { 15 | void v1628(Section *, const nbt::NBT &); 16 | 17 | void v2534(Section *, const nbt::NBT &); 18 | 19 | void v2840(Section *, const nbt::NBT &); 20 | 21 | void v3100(Section *, const nbt::NBT &); 22 | 23 | void catchall(Section *, const nbt::NBT &); 24 | } // namespace init_versions 25 | } // namespace versions 26 | } // namespace mcmap 27 | -------------------------------------------------------------------------------- /src/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ADD_EXECUTABLE(mcmap cli.cpp parse.cpp) 2 | TARGET_LINK_LIBRARIES(mcmap mcmap_core) 3 | 4 | IF (NOT DEBUG_BUILD) 5 | SET_TARGET_PROPERTIES(mcmap PROPERTIES LINK_FLAGS -s) 6 | ENDIF() 7 | -------------------------------------------------------------------------------- /src/cli/cli.cpp: -------------------------------------------------------------------------------- 1 | #include "../mcmap.h" 2 | #include "./parse.h" 3 | 4 | #include 5 | 6 | #define SUCCESS 0 7 | #define ERROR 1 8 | 9 | void printHelp(char *binary) { 10 | fmt::print( 11 | "Usage: {} WORLDPATH\n\n" 12 | " -from X Z coordinates of the block to start rendering at\n" 13 | " -to X Z coordinates of the block to stop rendering at\n" 14 | " -min/max VAL minimum/maximum Y index of blocks to render\n" 15 | " -file NAME output file; default is 'output.png'\n" 16 | " -colors NAME color file to use; default is 'colors.json'\n" 17 | " -nw -ne -se -sw the orientation of the map\n" 18 | " -nether render the nether\n" 19 | " -end render the end\n" 20 | " -dim[ension] NAME render a dimension by namespaced ID\n" 21 | " -nowater do not render water\n" 22 | " -nobeacons do not render beacon beams\n" 23 | " -shading toggle shading (brightens depending on height)\n" 24 | " -lighting toggle lighting (brightens depending on light)\n" 25 | " -mb int (=3500) use the specified amount of memory (in MB)\n" 26 | " -fragment int (=1024) render terrain in tiles of the specified size\n" 27 | " -tile int (=0) if not 0, will create split output of the " 28 | "desired tile size\n" 29 | " -marker X Z color draw a marker at X Z of the desired color\n" 30 | " -padding int (=5) padding to use around the image\n" 31 | " -h[elp] display an option summary\n" 32 | " -v[erbose] toggle debug mode (-vv for more)\n" 33 | " -dumpcolors dump a json with all defined colors\n" 34 | " -radius VAL radius of the circular render\n" 35 | " -centre|-center X Z coordinates of the centre of circular render\n", 36 | binary); 37 | } 38 | 39 | int main(int argc, char **argv) { 40 | Settings::WorldOptions options; 41 | Colors::Palette colors; 42 | 43 | auto logger = spdlog::stderr_color_mt("mcmap_cli"); 44 | spdlog::set_default_logger(logger); 45 | 46 | if (argc < 2 || !parseArgs(argc, argv, &options)) { 47 | printHelp(argv[0]); 48 | return ERROR; 49 | } 50 | 51 | // Load colors from the text segment 52 | Colors::load(&colors); 53 | 54 | // If requested, load colors from file 55 | if (!options.colorFile.empty()) 56 | Colors::load(&colors, options.colorFile); 57 | 58 | if (options.mode == Settings::DUMPCOLORS) { 59 | fmt::print("{}", json(colors).dump()); 60 | return SUCCESS; 61 | } else { 62 | fmt::print("{}\n", mcmap::version()); 63 | } 64 | 65 | // Overwrite water if asked to 66 | // TODO expand this to other blocks 67 | if (options.hideWater) 68 | colors["minecraft:water"] = Colors::Block(); 69 | if (options.hideBeacons) 70 | colors["mcmap:beacon_beam"] = Colors::Block(); 71 | 72 | if (!mcmap::render(options, colors, Progress::Status::ascii)) { 73 | logger::error("Error rendering terrain."); 74 | return ERROR; 75 | } 76 | 77 | fmt::print("Job complete.\n"); 78 | return SUCCESS; 79 | } 80 | -------------------------------------------------------------------------------- /src/cli/parse.cpp: -------------------------------------------------------------------------------- 1 | #include "./parse.h" 2 | 3 | #define ISPATH(p) (!(p).empty() && std::filesystem::exists((p))) 4 | 5 | bool parseArgs(int argc, char **argv, Settings::WorldOptions *opts) { 6 | #define MOREARGS(x) (argpos + (x) < argc) 7 | #define NEXTARG argv[++argpos] 8 | #define POLLARG(x) argv[argpos + (x)] 9 | int argpos = 0; 10 | while (MOREARGS(1)) { 11 | const char *option = NEXTARG; 12 | if (strcmp(option, "-from") == 0) { 13 | if (!MOREARGS(2) || !isNumeric(POLLARG(1)) || !isNumeric(POLLARG(2))) { 14 | logger::error("{} needs two integer arguments", option); 15 | return false; 16 | } 17 | opts->boundaries.minX = atoi(NEXTARG); 18 | opts->boundaries.minZ = atoi(NEXTARG); 19 | } else if (strcmp(option, "-to") == 0) { 20 | if (!MOREARGS(2) || !isNumeric(POLLARG(1)) || !isNumeric(POLLARG(2))) { 21 | logger::error("{} needs two integer arguments", option); 22 | return false; 23 | } 24 | opts->boundaries.maxX = atoi(NEXTARG); 25 | opts->boundaries.maxZ = atoi(NEXTARG); 26 | } else if (strcmp(option, "-centre") == 0 || 27 | strcmp(option, "-center") == 0) { 28 | if (!MOREARGS(2) || !isNumeric(POLLARG(1)) || !isNumeric(POLLARG(2))) { 29 | logger::error("{} needs two integer arguments", option); 30 | return false; 31 | } 32 | opts->boundaries.cenX = atoi(NEXTARG); 33 | opts->boundaries.cenZ = atoi(NEXTARG); 34 | } else if (strcmp(option, "-radius") == 0) { 35 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 36 | logger::error("{} needs an integer argument", option); 37 | return false; 38 | } 39 | opts->boundaries.radius = atoi(NEXTARG); 40 | } else if (strcmp(option, "-max") == 0) { 41 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 42 | logger::error("{} needs an integer argument", option); 43 | return false; 44 | } 45 | const int height = atoi(NEXTARG); 46 | opts->boundaries.maxY = 47 | std::min(height, static_cast(mcmap::constants::max_y)); 48 | } else if (strcmp(option, "-min") == 0) { 49 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 50 | logger::error("{} needs an integer argument", option); 51 | return false; 52 | } 53 | const int height = atoi(NEXTARG); 54 | opts->boundaries.minY = 55 | std::max(height, static_cast(mcmap::constants::min_y)); 56 | } else if (strcmp(option, "-padding") == 0) { 57 | if (!MOREARGS(1) || !isNumeric(POLLARG(1)) || atoi(POLLARG(1)) < 0) { 58 | logger::error("{} needs an positive integer argument", option); 59 | return false; 60 | } 61 | opts->padding = atoi(NEXTARG); 62 | } else if (strcmp(option, "-nowater") == 0) { 63 | opts->hideWater = true; 64 | } else if (strcmp(option, "-nobeacons") == 0) { 65 | opts->hideBeacons = true; 66 | } else if (strcmp(option, "-shading") == 0) { 67 | opts->shading = true; 68 | } else if (strcmp(option, "-lighting") == 0) { 69 | opts->lighting = true; 70 | } else if (strcmp(option, "-nether") == 0) { 71 | opts->dim = Dimension("the_nether"); 72 | } else if (strcmp(option, "-end") == 0) { 73 | opts->dim = Dimension("the_end"); 74 | } else if (strcmp(option, "-dimension") == 0 || 75 | strcmp(option, "-dim") == 0) { 76 | if (!MOREARGS(1)) { 77 | logger::error("{} needs a dimension name or number", option); 78 | return false; 79 | } 80 | opts->dim = Dimension(NEXTARG); 81 | } else if (strcmp(option, "-file") == 0) { 82 | if (!MOREARGS(1)) { 83 | logger::error("{} needs one argument", option); 84 | return false; 85 | } 86 | opts->outFile = NEXTARG; 87 | } else if (strcmp(option, "-colors") == 0) { 88 | if (!MOREARGS(1)) { 89 | logger::error("{} needs one argument", option); 90 | return false; 91 | } 92 | opts->colorFile = NEXTARG; 93 | if (!ISPATH(opts->colorFile)) { 94 | logger::error("File {} does not exist", opts->colorFile.string()); 95 | return false; 96 | } 97 | } else if (strcmp(option, "-dumpcolors") == 0) { 98 | opts->mode = Settings::DUMPCOLORS; 99 | } else if (strcmp(option, "-marker") == 0) { 100 | if (!MOREARGS(3) || !(isNumeric(POLLARG(1)) && isNumeric(POLLARG(2)))) { 101 | logger::error("{} needs three arguments: x z color", option); 102 | return false; 103 | } 104 | int x = atoi(NEXTARG), z = atoi(NEXTARG); 105 | opts->markers[opts->totalMarkers++] = 106 | Colors::Marker(x, z, std::string(NEXTARG)); 107 | } else if (strcmp(option, "-nw") == 0) { 108 | opts->boundaries.orientation = Map::NW; 109 | } else if (strcmp(option, "-sw") == 0) { 110 | opts->boundaries.orientation = Map::SW; 111 | } else if (strcmp(option, "-ne") == 0) { 112 | opts->boundaries.orientation = Map::NE; 113 | } else if (strcmp(option, "-se") == 0) { 114 | opts->boundaries.orientation = Map::SE; 115 | } else if (strcmp(option, "-mb") == 0) { 116 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 117 | logger::error("{} needs an integer", option); 118 | return false; 119 | } 120 | opts->mem_limit = atoi(NEXTARG) * size_t(1024 * 1024); 121 | } else if (strcmp(option, "-tile") == 0) { 122 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 123 | logger::error("{} needs an integer", option); 124 | return false; 125 | } 126 | opts->tile_size = atoi(NEXTARG); 127 | } else if (strcmp(option, "-fragment") == 0) { 128 | if (!MOREARGS(1) || !isNumeric(POLLARG(1))) { 129 | logger::error("{} needs an integer", option); 130 | return false; 131 | } 132 | opts->fragment_size = atoi(NEXTARG); 133 | } else if (strcmp(option, "-help") == 0 || strcmp(option, "-h") == 0) { 134 | opts->mode = Settings::HELP; 135 | return false; 136 | } else if (strcmp(option, "-verbose") == 0 || strcmp(option, "-v") == 0) { 137 | logger::set_level(spdlog::level::debug); 138 | } else if (strcmp(option, "-vv") == 0) { 139 | logger::set_level(spdlog::level::trace); 140 | } else { 141 | opts->save = SaveFile(option); 142 | } 143 | } 144 | 145 | if (opts->boundaries.circleDefined()) { 146 | // Generate the min/max coordinates based on our centre and the radius. 147 | // Add a little padding for good luck. 148 | int paddedRadius = 1.2 * opts->boundaries.radius; 149 | 150 | opts->boundaries.minX = opts->boundaries.cenX - paddedRadius; 151 | opts->boundaries.maxX = opts->boundaries.cenX + paddedRadius; 152 | opts->boundaries.minZ = opts->boundaries.cenZ - paddedRadius; 153 | opts->boundaries.maxZ = opts->boundaries.cenZ + paddedRadius; 154 | 155 | // We use the squared radius many times later; calculate it once here. 156 | opts->boundaries.rsqrd = opts->boundaries.radius * opts->boundaries.radius; 157 | } 158 | 159 | if (opts->mode == Settings::RENDER) { 160 | // Check if the given save posesses the required dimension, must be done now 161 | // as the world path can be given after the dimension name, which messes up 162 | // regionDir() 163 | if (!opts->save.valid()) { 164 | logger::error("Given folder does not seem to be a save file"); 165 | return false; 166 | } 167 | 168 | // Scan the region directory and map the existing terrain in this set of 169 | // coordinates 170 | World::Coordinates existingWorld = opts->save.getWorld(opts->dim); 171 | 172 | if (opts->boundaries.isUndefined()) { 173 | // No boundaries were defined, import the whole existing world 174 | // No overwriting to preserve potential min/max data 175 | opts->boundaries.minX = existingWorld.minX; 176 | opts->boundaries.minZ = existingWorld.minZ; 177 | opts->boundaries.maxX = existingWorld.maxX; 178 | opts->boundaries.maxZ = existingWorld.maxZ; 179 | } else { 180 | // Restrict the map to draw to the existing terrain 181 | opts->boundaries.crop(existingWorld); 182 | } 183 | 184 | if (opts->boundaries.maxX < opts->boundaries.minX || 185 | opts->boundaries.maxZ < opts->boundaries.minZ) { 186 | logger::debug("Processed boundaries: {}", opts->boundaries.to_string()); 187 | logger::error("Nothing to render: -from X Z has to be <= -to X Z"); 188 | return false; 189 | } 190 | 191 | if (opts->boundaries.maxX - opts->boundaries.minX < 0) { 192 | logger::error("Nothing to render: -min Y has to be < -max Y"); 193 | return false; 194 | } 195 | 196 | if (opts->fragment_size < 16) { 197 | logger::error("Cannot render map fragments this small"); 198 | return false; 199 | } 200 | 201 | if (opts->tile_size) { 202 | // In case tiling output has been queried 203 | // Forbid padding 204 | if (opts->padding != Settings::PADDING_DEFAULT && opts->padding) { 205 | logger::error("Cannot pad tiled output !"); 206 | return false; 207 | } 208 | 209 | // Change output.png to output by default 210 | if (opts->outFile == Settings::OUTPUT_DEFAULT) 211 | opts->outFile = Settings::OUTPUT_TILED_DEFAULT; 212 | 213 | // Get absolute path towards file 214 | if (opts->outFile.is_relative()) 215 | opts->outFile = fs::absolute(opts->outFile); 216 | 217 | std::error_code dir_creation_error; 218 | fs::create_directory(opts->outFile, dir_creation_error); 219 | if (dir_creation_error) { 220 | logger::error("Failed to create directory {}: {}", 221 | opts->outFile.string().c_str(), 222 | dir_creation_error.message()); 223 | return false; 224 | } 225 | } 226 | } 227 | 228 | return true; 229 | } 230 | -------------------------------------------------------------------------------- /src/cli/parse.h: -------------------------------------------------------------------------------- 1 | #include "../settings.h" 2 | 3 | bool parseArgs(int, char **, Settings::WorldOptions *); 4 | -------------------------------------------------------------------------------- /src/colors.cpp: -------------------------------------------------------------------------------- 1 | #include "colors.h" 2 | 3 | std::map erroneous; 4 | 5 | namespace Colors { 6 | // Embedded colors, as a byte array. This array is created by compiling 7 | // `colors.json` into `colors.bson`, using `json2bson`, then included here. The 8 | // json library can then interpret it into a usable `Palette` object 9 | const std::vector default_colors = 10 | #include "colors.bson" 11 | ; 12 | } // namespace Colors 13 | 14 | bool Colors::load(Palette *colors, const json &data) { 15 | Palette defined; 16 | 17 | try { 18 | defined = data.get(); 19 | } catch (const nlohmann::detail::parse_error &err) { 20 | logger::error("Parsing JSON data failed: {}", err.what()); 21 | return false; 22 | } catch (const std::invalid_argument &err) { 23 | logger::error("Parsing JSON data failed: {}", err.what()); 24 | return false; 25 | } 26 | 27 | for (const auto &overriden : defined) 28 | colors->insert_or_assign(overriden.first, overriden.second); 29 | 30 | return true; 31 | } 32 | 33 | // Load colors from file into the palette passed as an argument 34 | bool Colors::load(Palette *colors, const fs::path &color_file) { 35 | json colors_j; 36 | 37 | if (color_file.empty() || !fs::exists(color_file)) { 38 | logger::error("Could not open color file `{}`", color_file.string()); 39 | return false; 40 | } 41 | 42 | FILE *f = fopen(color_file.string().c_str(), "r"); 43 | 44 | try { 45 | colors_j = json::parse(f); 46 | } catch (const nlohmann::detail::parse_error &err) { 47 | logger::error("Parsing color file `{}` failed: {}", color_file.string(), 48 | err.what()); 49 | fclose(f); 50 | return false; 51 | } 52 | fclose(f); 53 | 54 | bool status = load(colors, colors_j); 55 | 56 | if (!status) 57 | logger::error("From file `{}`", color_file.string()); 58 | 59 | return true; 60 | } 61 | 62 | void Colors::to_json(json &data, const Color &c) { 63 | data = fmt::format("{:c}", c); 64 | } 65 | 66 | void Colors::from_json(const json &data, Color &c) { 67 | if (data.is_string()) { 68 | c = Colors::Color(data.get()); 69 | } else if (data.is_array()) { 70 | c = Colors::Color(data.get>()); 71 | } 72 | } 73 | 74 | void Colors::to_json(json &j, const Block &b) { 75 | if (b.type == Colors::BlockTypes::FULL) { 76 | j = json(fmt::format("{:c}", b.primary)); 77 | return; 78 | } 79 | 80 | string type = typeToString.at(b.type); 81 | 82 | j = json{{"type", type}, {"color", b.primary}}; 83 | 84 | if (!b.secondary.empty()) 85 | j["accent"] = b.secondary; 86 | } 87 | 88 | void Colors::from_json(const json &data, Block &b) { 89 | string stype; 90 | 91 | // If the definition is an array, the block is a full block with a single 92 | // color 93 | if (data.is_string() || data.is_array()) { 94 | b = Block(BlockTypes::FULL, data); 95 | return; 96 | } 97 | 98 | // If the definition is an object and there is no color, replace it with air 99 | if (data.find("color") == data.end()) { 100 | b = Block(); 101 | throw(std::invalid_argument(fmt::format( 102 | "Wrong color format: no color attribute found in `{}`", data.dump()))); 103 | } 104 | 105 | // If the type is illegal, default it with a full block 106 | if (data.find("type") == data.end()) { 107 | if (data.is_string() || data.is_array()) { 108 | b = Block(BlockTypes::FULL, data["color"]); 109 | return; 110 | } 111 | } 112 | 113 | stype = data["type"].get(); 114 | if (Colors::stringToType.find(stype) == stringToType.end()) { 115 | auto pair = erroneous.find(stype); 116 | if (pair == erroneous.end()) { 117 | logger::warn("Block with type {} is either disabled or not implemented", 118 | stype); 119 | erroneous.insert(std::pair(stype, 1)); 120 | } else 121 | pair->second++; 122 | 123 | b = Block(BlockTypes::FULL, data["color"]); 124 | return; 125 | } 126 | 127 | BlockTypes type = stringToType.at(data["type"].get()); 128 | 129 | if (data.find("accent") != data.end()) 130 | b = Block(type, data["color"], data["accent"]); 131 | else 132 | b = Block(type, data["color"]); 133 | } 134 | 135 | void Colors::to_json(json &j, const Palette &p) { 136 | for (auto it : p) 137 | j.emplace(it.first, json(it.second)); 138 | } 139 | 140 | void Colors::from_json(const json &j, Palette &p) { 141 | for (auto it : j.get>()) 142 | p.emplace(it.first, it.second.get()); 143 | } 144 | -------------------------------------------------------------------------------- /src/colors.h: -------------------------------------------------------------------------------- 1 | #ifndef COLORS_ 2 | #define COLORS_ 3 | 4 | #include "./helper.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using nlohmann::json; 14 | using std::list; 15 | using std::map; 16 | using std::string; 17 | 18 | #define PRED 0 19 | #define PGREEN 1 20 | #define PBLUE 2 21 | #define PALPHA 3 22 | 23 | inline void blend(uint8_t *const destination, const uint8_t *const source) { 24 | if (!source[PALPHA]) 25 | return; 26 | 27 | if (destination[PALPHA] == 0 || source[PALPHA] == 255) { 28 | memcpy(destination, source, 4); 29 | return; 30 | } 31 | #define BLEND(ca, aa, cb) \ 32 | uint8_t(((size_t(ca) * size_t(aa)) + (size_t(255 - aa) * size_t(cb))) / 255) 33 | destination[0] = BLEND(source[0], source[PALPHA], destination[0]); 34 | destination[1] = BLEND(source[1], source[PALPHA], destination[1]); 35 | destination[2] = BLEND(source[2], source[PALPHA], destination[2]); 36 | destination[PALPHA] += 37 | (size_t(source[PALPHA]) * size_t(255 - destination[PALPHA])) / 255; 38 | #undef BLEND 39 | } 40 | 41 | inline void addColor(uint8_t *const color, const uint8_t *const add) { 42 | const float v2 = (float(add[PALPHA]) / 255.0f); 43 | const float v1 = (1.0f - (v2 * .2f)); 44 | color[0] = clamp(uint16_t(float(color[0]) * v1 + float(add[0]) * v2)); 45 | color[1] = clamp(uint16_t(float(color[1]) * v1 + float(add[1]) * v2)); 46 | color[2] = clamp(uint16_t(float(color[2]) * v1 + float(add[2]) * v2)); 47 | } 48 | 49 | namespace Colors { 50 | 51 | enum BlockTypes { 52 | #define DEFINETYPE(STRING, CALLBACK) CALLBACK, 53 | FULL = 0, 54 | #include "blocktypes.def" 55 | #undef DEFINETYPE 56 | }; 57 | 58 | const std::unordered_map stringToType = { 59 | {"Full", Colors::BlockTypes::FULL}, 60 | #define DEFINETYPE(STRING, CALLBACK) {STRING, Colors::BlockTypes::CALLBACK}, 61 | #include "blocktypes.def" 62 | #undef DEFINETYPE 63 | }; 64 | 65 | const std::unordered_map typeToString = { 66 | {Colors::BlockTypes::FULL, "Full"}, 67 | #define DEFINETYPE(STRING, CALLBACK) {Colors::BlockTypes::CALLBACK, STRING}, 68 | #include "blocktypes.def" 69 | #undef DEFINETYPE 70 | }; 71 | 72 | const std::map> markerColors = { 73 | {"white", {250, 250, 250, 100}}, 74 | {"red", {250, 0, 0, 100}}, 75 | {"green", {0, 250, 0, 100}}, 76 | {"blue", {0, 0, 250, 100}}, 77 | }; 78 | 79 | struct Color { 80 | // Red, Green, Blue, Transparency 81 | uint8_t R, G, B, ALPHA; 82 | 83 | Color() { R = G = B = ALPHA = 0; } 84 | 85 | Color(const std::string &code) : Color() { 86 | if (code[0] != '#' || (code.size() != 7 && code.size() != 9)) 87 | throw std::invalid_argument(fmt::format("Invalid color code: {}", code)); 88 | 89 | R = std::stoi(code.substr(1, 2), NULL, 16); 90 | G = std::stoi(code.substr(3, 2), NULL, 16); 91 | B = std::stoi(code.substr(5, 2), NULL, 16); 92 | 93 | if (code.size() == 9) 94 | ALPHA = std::stoi(code.substr(7, 2), NULL, 16); 95 | else 96 | ALPHA = 255; 97 | } 98 | 99 | Color(const char *code) : Color(std::string(code)){}; 100 | 101 | Color(list values) : Color() { 102 | uint8_t index = 0; 103 | // Hacky hacky stuff 104 | // convert the struct to a uint8_t list to fill its elements 105 | // as we know uint8_t elements will be contiguous in memory 106 | for (auto it : values) 107 | if (index < 6) 108 | ((uint8_t *)this)[index++] = it; 109 | } 110 | 111 | inline void modColor(const int mod) { 112 | R = clamp(R + mod); 113 | G = clamp(G + mod); 114 | B = clamp(B + mod); 115 | } 116 | 117 | bool empty() const { return !(R || G || B || ALPHA); } 118 | bool transparent() const { return !ALPHA; } 119 | bool opaque() const { return ALPHA == 255; } 120 | 121 | float brightness() const { 122 | return sqrt(double(R) * double(R) * .2126 + double(G) * double(G) * .7152 + 123 | double(B) * double(B) * .0722); 124 | } 125 | 126 | Color operator+(const Color &other) const { 127 | Color mix(*this); 128 | 129 | if (!mix.opaque()) 130 | addColor((uint8_t *)&mix, (uint8_t *)&other); 131 | 132 | return mix; 133 | } 134 | 135 | bool operator==(const Color &other) const { 136 | return R == other.R && B == other.B && G == other.G; 137 | } 138 | }; 139 | 140 | struct Block { 141 | Colors::Color primary, secondary; // 8 bytes 142 | Colors::BlockTypes type; 143 | Colors::Color light, dark; // 8 bytes 144 | 145 | Block() : primary(), secondary() { type = Colors::BlockTypes::FULL; } 146 | 147 | Block(const Colors::BlockTypes &bt, const Colors::Color &c1) 148 | : primary(c1), secondary(), light(c1), dark(c1) { 149 | type = bt; 150 | light.modColor(mcmap::constants::color_offset_right); 151 | dark.modColor(mcmap::constants::color_offset_left); 152 | } 153 | 154 | Block(const Colors::BlockTypes &bt, const Colors::Color &c1, 155 | const Colors::Color &c2) 156 | : Block(bt, c1) { 157 | secondary = c2; 158 | } 159 | 160 | Block operator+(const Block &other) const { 161 | Block mix; 162 | mix.type = this->type; 163 | mix.primary = this->primary + other.primary; 164 | mix.secondary = this->secondary + other.secondary; 165 | 166 | return mix; 167 | } 168 | 169 | bool operator==(const Block &other) const { 170 | return memcmp(this, &other, 12) == 0; 171 | } 172 | 173 | bool operator!=(const Block &other) const { return !operator==(other); } 174 | 175 | Block shade(float fsub) const NOINLINE { 176 | Block shaded = *this; 177 | 178 | shaded.primary.modColor(fsub * (primary.brightness() / 323.0f + .21f)); 179 | shaded.secondary.modColor(fsub * (secondary.brightness() / 323.0f + .21f)); 180 | shaded.dark.modColor(fsub * (dark.brightness() / 323.0f + .21f)); 181 | shaded.light.modColor(fsub * (light.brightness() / 323.0f + .21f)); 182 | 183 | return shaded; 184 | } 185 | }; 186 | 187 | typedef map Palette; 188 | 189 | struct Marker { 190 | int64_t x, z; 191 | Block color; 192 | 193 | Marker() { 194 | x = std::numeric_limits::max(); 195 | z = std::numeric_limits::max(); 196 | } 197 | 198 | Marker(int64_t x, int64_t z, string c) : x(x), z(z) { 199 | if (markerColors.find(c) == markerColors.end()) { 200 | logger::error("Invalid marker color: {}", c); 201 | c = "white"; 202 | } 203 | 204 | color = Block(BlockTypes::drawBeam, markerColors.find(c)->second); 205 | }; 206 | }; 207 | 208 | extern const std::vector default_colors; 209 | 210 | bool load(Palette *, const json & = json::from_bson(default_colors)); 211 | bool load(Palette *, const fs::path &); 212 | 213 | void to_json(json &, const Color &); 214 | void from_json(const json &, Color &); 215 | 216 | void to_json(json &j, const Block &b); 217 | void from_json(const json &j, Block &b); 218 | 219 | void to_json(json &j, const Palette &p); 220 | void from_json(const json &j, Palette &p); 221 | 222 | } // namespace Colors 223 | 224 | template <> struct fmt::formatter : formatter { 225 | char presentation = 'c'; 226 | constexpr auto parse(format_parse_context &ctx) { 227 | auto it = ctx.begin(), end = ctx.end(); 228 | 229 | if (it != end && *it == 'c') 230 | presentation = *it++; 231 | 232 | // Check if reached the end of the range: 233 | if (it != end && *it != '}') 234 | throw format_error("invalid format"); 235 | 236 | // Return an iterator past the end of the parsed range: 237 | return it; 238 | } 239 | 240 | auto format(const Colors::Color &c, format_context &ctx) const { 241 | if (c.ALPHA == 0xff) 242 | return format_to(ctx.out(), "#{:02x}{:02x}{:02x}", c.R, c.G, c.B); 243 | else 244 | return format_to(ctx.out(), "#{:02x}{:02x}{:02x}{:02x}", c.R, c.G, c.B, 245 | c.ALPHA); 246 | } 247 | }; 248 | 249 | #endif // COLORS_H_ 250 | -------------------------------------------------------------------------------- /src/graphical/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | SET(CMAKE_INCLUDE_CURRENT_DIR ON) 2 | 3 | IF(Qt6_FOUND) 4 | SET(CMAKE_AUTOUIC ON) 5 | SET(CMAKE_AUTOMOC ON) 6 | SET(CMAKE_AUTORCC ON) 7 | 8 | SET(ICON_INDEX_FILE icons.qrc) 9 | 10 | FUNCTION(INDEX_RESOURCES OUTPUT PATH) 11 | FILE(GLOB RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${PATH}/*) 12 | FILE(WRITE ${OUTPUT} "") 13 | FOREACH(RESOURCE ${RESOURCES}) 14 | FILE(APPEND ${OUTPUT} "${RESOURCE}") 15 | ENDFOREACH() 16 | FILE(APPEND ${OUTPUT} "") 17 | ENDFUNCTION() 18 | 19 | INDEX_RESOURCES(${ICON_INDEX_FILE} icons) 20 | 21 | SET(GUI_SOURCES 22 | main.cpp 23 | mainwindow.cpp 24 | mainwindow.h 25 | mainwindow.ui 26 | ${ICON_INDEX_FILE}) 27 | 28 | IF(NOT WIN32) 29 | ADD_EXECUTABLE(mcmap-gui ${GUI_SOURCES}) 30 | ELSE() 31 | ADD_EXECUTABLE(mcmap-gui WIN32 ${GUI_SOURCES}) 32 | ENDIF() 33 | 34 | TARGET_LINK_LIBRARIES(mcmap-gui 35 | PRIVATE 36 | Qt6::Widgets 37 | ZLIB::ZLIB 38 | PNG::PNG 39 | fmt::fmt-header-only 40 | mcmap_core) 41 | ENDIF() 42 | -------------------------------------------------------------------------------- /src/graphical/icons/deepslate_diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/deepslate_diamond.png -------------------------------------------------------------------------------- /src/graphical/icons/deepslate_redstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/deepslate_redstone.png -------------------------------------------------------------------------------- /src/graphical/icons/grass_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/grass_block.png -------------------------------------------------------------------------------- /src/graphical/icons/lapis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/lapis.png -------------------------------------------------------------------------------- /src/graphical/icons/lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/lava.png -------------------------------------------------------------------------------- /src/graphical/icons/sprout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoutn1k/mcmap/6b865af63231aef9d97699f6f26dc4dd64203c7d/src/graphical/icons/sprout.png -------------------------------------------------------------------------------- /src/graphical/main.cpp: -------------------------------------------------------------------------------- 1 | #include "../colors.h" 2 | #include "mainwindow.h" 3 | #include 4 | #include 5 | 6 | Colors::Palette default_palette; 7 | 8 | int main(int argc, char *argv[]) { 9 | Colors::load(&default_palette); 10 | 11 | QApplication a(argc, argv); 12 | MainWindow w; 13 | w.show(); 14 | return a.exec(); 15 | } 16 | -------------------------------------------------------------------------------- /src/graphical/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | namespace Ui { 12 | class MainWindow; 13 | } 14 | QT_END_NAMESPACE 15 | 16 | class Renderer : public QObject { 17 | Q_OBJECT 18 | QThread renderThread; 19 | 20 | public slots: 21 | void render(); 22 | 23 | signals: 24 | void startRender(); 25 | void resultReady(); 26 | void sendProgress(int, int, int); 27 | }; 28 | 29 | class MainWindow : public QMainWindow { 30 | Q_OBJECT 31 | 32 | public: 33 | MainWindow(QWidget *parent = nullptr); 34 | ~MainWindow(); 35 | 36 | private: 37 | QThread renderThread; 38 | QPlainTextEdit *log_messages; 39 | 40 | std::shared_ptr logger = nullptr; 41 | void closeEvent(QCloseEvent *); 42 | 43 | private slots: 44 | void on_renderButton_clicked(); 45 | 46 | void on_saveSelectButton_clicked(); 47 | void on_singlePNGFileSelect_clicked(); 48 | void on_tiledOutputFileSelect_clicked(); 49 | void on_colorSelectButton_clicked(); 50 | void on_colorResetButton_clicked(); 51 | 52 | void on_orientationNW_toggled(bool); 53 | void on_orientationSW_toggled(bool); 54 | void on_orientationSE_toggled(bool); 55 | void on_orientationNE_toggled(bool); 56 | 57 | void on_dimensionSelectDropDown_currentIndexChanged(int index); 58 | 59 | void on_boxMinX_textEdited(const QString &); 60 | void on_boxMaxX_textEdited(const QString &); 61 | void on_boxMinZ_textEdited(const QString &); 62 | void on_boxMaxZ_textEdited(const QString &); 63 | void on_boxMinY_textEdited(const QString &); 64 | void on_boxMaxY_textEdited(const QString &); 65 | 66 | void on_circularCenterX_textEdited(const QString &); 67 | void on_circularCenterZ_textEdited(const QString &); 68 | void on_circularMinY_textEdited(const QString &); 69 | void on_circularMaxY_textEdited(const QString &); 70 | void on_circularRadius_textEdited(const QString &); 71 | 72 | void on_paddingValue_valueChanged(int arg1); 73 | 74 | void on_shading_stateChanged(int); 75 | void on_lighting_stateChanged(int); 76 | 77 | void startRender(); 78 | void stopRender(); 79 | void updateProgress(int, int, int); 80 | 81 | void on_actionToggleLogs_triggered(); 82 | void on_actionDumpColors_triggered(); 83 | void on_actionExit_triggered(); 84 | void on_actionVersion_triggered(); 85 | void on_actionSetMemoryLimit_triggered(); 86 | void on_actionSetFragmentSize_triggered(); 87 | 88 | signals: 89 | void render(); 90 | 91 | private: 92 | Ui::MainWindow *ui; 93 | }; 94 | 95 | #endif // MAINWINDOW_H 96 | -------------------------------------------------------------------------------- /src/helper.cpp: -------------------------------------------------------------------------------- 1 | #include "helper.h" 2 | #include 3 | 4 | uint32_t _ntohl(uint8_t *val) { 5 | return (uint32_t(val[0]) << 24) + (uint32_t(val[1]) << 16) + 6 | (uint32_t(val[2]) << 8) + (uint32_t(val[3])); 7 | } 8 | 9 | uint8_t clamp(int32_t val) { 10 | if (val < 0) { 11 | return 0; 12 | } 13 | if (val > 255) { 14 | return 255; 15 | } 16 | return (uint8_t)val; 17 | } 18 | 19 | bool isNumeric(const char *str) { 20 | if (str[0] == '-' && str[1] != '\0') { 21 | ++str; 22 | } 23 | while (*str != '\0') { 24 | if (*str < '0' || *str > '9') { 25 | return false; 26 | } 27 | ++str; 28 | } 29 | return true; 30 | } 31 | 32 | size_t memory_capacity(size_t limit, size_t element_size, size_t elements, 33 | size_t threads) { 34 | // Reserve 60K for variables and stuff 35 | const size_t overhead = 60 * size_t(1024 * 1024); 36 | // Rendering requires at least this amount 37 | const size_t rendering = std::min(threads, elements) * element_size; 38 | 39 | // Check we have enought memory 40 | if (limit < overhead + rendering) { 41 | logger::error( 42 | "At least {:.2f}MB are required to render with those parameters", 43 | float(overhead + rendering) / float(1024 * 1024)); 44 | return 0; 45 | } 46 | 47 | // Return the amount of canvasses that fit in memory - including the ones 48 | // being rendered 49 | return (limit - overhead - rendering) / element_size; 50 | } 51 | 52 | bool prepare_cache(const std::filesystem::path &cache) { 53 | // If we can create the directory, no more checks 54 | if (create_directory(cache)) 55 | return true; 56 | 57 | fs::file_status cache_status = status(cache); 58 | fs::perms required = fs::perms::owner_all; 59 | 60 | if (cache_status.type() != fs::file_type::directory) { 61 | logger::error("Cache directory `{}` is not a directory", cache.string()); 62 | return false; 63 | } 64 | 65 | if ((cache_status.permissions() & required) != required) { 66 | logger::error("Cache directory `{}` does not have the right permissions", 67 | cache.string()); 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | fs::path getHome() { 75 | #ifndef _WINDOWS 76 | char target[] = "HOME"; 77 | char *query = getenv(target); 78 | 79 | if (!query) 80 | return ""; 81 | 82 | return std::string(query); 83 | #else 84 | char drive[] = "HOMEDRIVE", dir[] = "HOMEPATH"; 85 | char *query_drive = getenv(drive), *query_path = getenv(dir); 86 | 87 | if (!query_drive || !query_path) 88 | return ""; 89 | 90 | return fs::path(std::string(query_drive)) / fs::path(std::string(query_path)); 91 | #endif 92 | } 93 | 94 | fs::path getSaveDir() { 95 | fs::path prefix, suffix = ".minecraft/saves"; 96 | 97 | #ifndef _WINDOWS 98 | prefix = getHome(); 99 | #else 100 | char target[] = "APPDATA"; 101 | 102 | char *query = getenv(target); 103 | 104 | if (!query) 105 | return ""; 106 | 107 | prefix = std::string(query); 108 | #endif 109 | 110 | if (!fs::exists(prefix)) 111 | return ""; 112 | 113 | return prefix / suffix; 114 | } 115 | 116 | fs::path getTempDir() { 117 | std::error_code error; 118 | 119 | fs::path destination = fs::temp_directory_path(error); 120 | 121 | if (destination.empty() || error) 122 | destination = fs::current_path(); 123 | 124 | return destination / mcmap::config::cache_name; 125 | } 126 | -------------------------------------------------------------------------------- /src/helper.h: -------------------------------------------------------------------------------- 1 | #ifndef HELPER_H_ 2 | #define HELPER_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace fs = std::filesystem; 8 | 9 | #if defined(__clang__) || defined(__GNUC__) 10 | #define NOINLINE __attribute__((noinline)) 11 | #else 12 | #define NOINLINE 13 | #endif 14 | 15 | #if defined(_OPENMP) && defined(_WINDOWS) 16 | #define OMP_FOR_INDEX int 17 | #else 18 | #define OMP_FOR_INDEX std::vector::size_type 19 | #endif 20 | 21 | #ifdef _WINDOWS 22 | #define FSEEK fseek 23 | #else 24 | #define FSEEK fseeko 25 | #endif 26 | 27 | #define CHUNKSIZE 16 28 | #define REGIONSIZE 32 29 | 30 | namespace mcmap { 31 | namespace constants { 32 | const int16_t min_y = -64; 33 | const int16_t max_y = 319; 34 | const uint16_t terrain_height = max_y - min_y + 1; 35 | 36 | const int8_t color_offset_left = -27; 37 | const int8_t color_offset_right = -17; 38 | 39 | const int8_t lighting_dark = -75; 40 | const int8_t lighting_bright = 100; 41 | const int8_t lighting_delta = (lighting_bright - lighting_dark) >> 4; 42 | } // namespace constants 43 | 44 | namespace config { 45 | const std::string cache_name = "mcmap_cache"; 46 | } 47 | } // namespace mcmap 48 | 49 | #define REGION_HEADER_SIZE REGIONSIZE *REGIONSIZE * 4 50 | #define DECOMPRESSED_BUFFER 1000 * 1024 51 | #define COMPRESSED_BUFFER 500 * 1024 52 | 53 | #define CHUNK(x) ((x) >> 4) 54 | #define REGION(x) ((x) >> 5) 55 | 56 | uint8_t clamp(int32_t val); 57 | bool isNumeric(const char *str); 58 | 59 | uint32_t _ntohl(uint8_t *val); 60 | 61 | size_t memory_capacity(size_t, size_t, size_t, size_t); 62 | bool prepare_cache(const fs::path &); 63 | 64 | fs::path getHome(); 65 | fs::path getSaveDir(); 66 | fs::path getTempDir(); 67 | 68 | #endif // HELPER_H_ 69 | -------------------------------------------------------------------------------- /src/include/2DCoordinates.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct Coordinates { 8 | int32_t x, z; 9 | 10 | template ::value, int> = 0> 11 | Coordinates(std::initializer_list l) { 12 | x = *l.begin(); 13 | z = *(l.begin() + 1); 14 | } 15 | 16 | Coordinates() { 17 | x = int32_t(); 18 | z = int32_t(); 19 | } 20 | 21 | Coordinates operator+(const Coordinates &other) const { 22 | return {x + other.x, z + other.z}; 23 | } 24 | 25 | bool operator<(const Coordinates &other) const { 26 | // Code from 27 | return x < other.x || (!(other.x < x) && z < other.z); 28 | } 29 | 30 | bool operator==(const Coordinates &other) const { 31 | return x == other.x && z == other.z; 32 | } 33 | 34 | bool operator!=(const Coordinates &other) const { 35 | return !this->operator==(other); 36 | } 37 | 38 | template ::value, int> = 0> 39 | Coordinates operator%(const I &m) const { 40 | return {x % m, z % m}; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/include/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | GET_FILE ( 2 | https://github.com/nlohmann/json/releases/download/v3.9.1/json.hpp 3 | ${CMAKE_SOURCE_DIR}/src/include/json.hpp 4 | MD5=5eabadfb8cf8fe1bf0811535c65f027f 5 | ) 6 | -------------------------------------------------------------------------------- /src/include/compat.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /* Loosely inspired from lower_bound in 5 | * /usr/include/c++/11.1.0/bits/stl_algobase.h 6 | * Returns a const_iterator to the highest integer that is inferior or equal to 7 | * `version` from the keys of a map */ 8 | template 9 | typename std::map::const_iterator 10 | compatible(const std::map &hash, int version) { 11 | typedef typename std::map::const_iterator _Iterator; 12 | typedef typename _Iterator::difference_type _DistanceType; 13 | 14 | _Iterator __first = hash.begin(), __end = hash.end(); 15 | _DistanceType __len = std::distance(__first, __end); 16 | 17 | while (__len > 1) { 18 | _DistanceType __half = __len >> 1; 19 | _Iterator __middle = __first; 20 | std::advance(__middle, __half); 21 | 22 | if (__middle->first > version) 23 | __end = __middle; 24 | else 25 | __first = __middle; 26 | 27 | __len = __len - __half; 28 | } 29 | 30 | return __first; 31 | } 32 | -------------------------------------------------------------------------------- /src/include/counter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | template ::value, int> = 0> 7 | struct Counter { 8 | UInteger counter; 9 | 10 | Counter(UInteger value = 0) : counter(value) {} 11 | 12 | Counter &operator++() { 13 | counter < std::numeric_limits::max() ? ++counter : counter; 14 | return *this; 15 | } 16 | 17 | operator UInteger() { return counter; } 18 | }; 19 | -------------------------------------------------------------------------------- /src/include/logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace logger { 7 | 8 | using spdlog::set_level; 9 | 10 | using spdlog::debug; 11 | using spdlog::error; 12 | using spdlog::info; 13 | using spdlog::trace; 14 | using spdlog::warn; 15 | 16 | } // namespace logger 17 | -------------------------------------------------------------------------------- /src/include/map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace Map { 6 | 7 | enum Orientation { 8 | NW = 0, 9 | SW, 10 | SE, 11 | NE, 12 | }; 13 | 14 | template ::value, int> = 0> 16 | struct Coordinates { 17 | Integer minX, maxX, minZ, maxZ; 18 | Integer cenX, cenZ, radius, rsqrd; 19 | int16_t minY, maxY; 20 | Orientation orientation; 21 | 22 | Coordinates() { 23 | setUndefined(); 24 | orientation = NW; 25 | } 26 | 27 | Coordinates(Integer _minX, int16_t _minY, Integer _minZ, Integer _maxX, 28 | int16_t _maxY, Integer _maxZ, Orientation o = NW) 29 | : minX(_minX), maxX(_maxX), minZ(_minZ), maxZ(_maxZ), minY(_minY), 30 | maxY(_maxY), orientation(o) {} 31 | 32 | void setUndefined() { 33 | minX = minZ = std::numeric_limits::max(); 34 | maxX = maxZ = std::numeric_limits::min(); 35 | minY = std::numeric_limits::max(); 36 | maxY = std::numeric_limits::min(); 37 | cenX = cenZ = radius = std::numeric_limits::max(); 38 | } 39 | 40 | bool isUndefined() const { 41 | return (minX == minZ && minX == std::numeric_limits::max() && 42 | maxX == maxZ && maxX == std::numeric_limits::min()); 43 | } 44 | 45 | // Only have a circle if we have all three parts. 46 | bool circleDefined() const { 47 | return (cenX != std::numeric_limits::max() && 48 | cenZ != std::numeric_limits::max() && 49 | radius != std::numeric_limits::max()); 50 | } 51 | 52 | void setMaximum() { 53 | setUndefined(); 54 | std::swap(minX, maxX); 55 | std::swap(minZ, maxZ); 56 | std::swap(minY, maxY); 57 | } 58 | 59 | void crop(const Coordinates &boundaries) { 60 | minX = std::max(minX, boundaries.minX); 61 | minZ = std::max(minZ, boundaries.minZ); 62 | maxX = std::min(maxX, boundaries.maxX); 63 | maxZ = std::min(maxZ, boundaries.maxZ); 64 | } 65 | 66 | inline bool intersects(const Coordinates &other) { 67 | return (((other.minX <= maxX && maxX <= other.maxX) || 68 | (other.minX <= minX && minX <= other.maxX)) && 69 | ((other.minZ <= maxZ && maxZ <= other.maxZ) || 70 | (other.minZ <= minZ && minZ <= other.maxZ))) || 71 | (((minX <= other.maxX && other.maxX <= maxX) || 72 | (minX <= other.minX && other.minX <= maxX)) && 73 | ((minZ <= other.maxZ && other.maxZ <= maxZ) || 74 | (minZ <= other.minZ && other.minZ <= maxZ))); 75 | } 76 | 77 | std::string to_string() const { 78 | std::string str_orient = "ERROR"; 79 | switch (orientation) { 80 | case NW: 81 | str_orient = "North-West"; 82 | break; 83 | case SW: 84 | str_orient = "South-West"; 85 | break; 86 | case SE: 87 | str_orient = "South-East"; 88 | break; 89 | case NE: 90 | str_orient = "North-East"; 91 | break; 92 | } 93 | 94 | #ifndef _WINDOWS 95 | const std::string format_str = "{}.{}.{} ~> {}.{}.{} ({})"; 96 | #else 97 | const std::string format_str = "{}.{}.{}.{}.{}.{}.{}"; 98 | #endif 99 | 100 | return fmt::format(format_str, minX, minZ, minY, maxX, maxZ, maxY, 101 | str_orient); 102 | } 103 | 104 | void rotate() { 105 | std::swap(minX, maxX); 106 | minX = -minX; 107 | maxX = -maxX; 108 | std::swap(minX, minZ); 109 | std::swap(maxX, maxZ); 110 | }; 111 | 112 | Coordinates orient(Orientation o) const { 113 | Coordinates oriented = *this; 114 | 115 | for (int i = 0; i < (4 + o - orientation) % 4; i++) 116 | oriented.rotate(); 117 | 118 | oriented.orientation = o; 119 | 120 | return oriented; 121 | }; 122 | 123 | inline Integer sizeX() const { return maxX - minX + 1; } 124 | inline Integer sizeZ() const { return maxZ - minZ + 1; } 125 | 126 | size_t footprint() const { 127 | Integer width = (sizeX() + sizeZ()) * 2; 128 | Integer height = sizeX() + sizeZ() + (maxY - minY + 1) * 3 - 1; 129 | 130 | #define BYTESPERPIXEL 4 131 | return width * height * BYTESPERPIXEL; 132 | #undef BYTESPERPIXEL 133 | } 134 | 135 | void fragment(std::vector> &fragments, 136 | size_t size) const { 137 | for (Integer x = minX; x <= maxX; x += size) { 138 | for (Integer z = minZ; z <= maxZ; z += size) { 139 | Coordinates fragment = *this; 140 | 141 | fragment.minX = x; 142 | fragment.maxX = std::min(Integer(x + size - 1), maxX); 143 | 144 | fragment.minZ = z; 145 | fragment.maxZ = std::min(Integer(z + size - 1), maxZ); 146 | 147 | fragments.push_back(fragment); 148 | } 149 | } 150 | } 151 | 152 | // The following methods are used to get the position of the map in an image 153 | // made by another (englobing) map, called the referential 154 | 155 | inline Integer offsetX(const Coordinates &referential) const { 156 | // This formula is thought around the top corner' position. 157 | // 158 | // The top corner's postition of the sub-map is influenced by its distance 159 | // to the full map's top corner => we compare the minX and minZ 160 | // coordinates 161 | // 162 | // From there, the map's top corner is sizeZ pizels from the edge, and the 163 | // sub-canvasses' edge is at sizeZ' pixels from its top corner. 164 | // 165 | // By adding up those elements we get the delta between the edge of the 166 | // full image and the edge of the partial image. 167 | Coordinates oriented = this->orient(Orientation::NW); 168 | 169 | return 2 * (referential.sizeZ() - oriented.sizeZ() - 170 | (referential.minX - oriented.minX) + 171 | (referential.minZ - oriented.minZ)); 172 | } 173 | 174 | inline Integer offsetY(const Coordinates &referential) const { 175 | // This one is simpler, the vertical distance being equal to the distance 176 | // between top corners. 177 | Coordinates oriented = this->orient(Orientation::NW); 178 | 179 | return oriented.minX - referential.minX + oriented.minZ - referential.minZ; 180 | } 181 | 182 | Coordinates &operator+=(const Coordinates &other) { 183 | minX = std::min(other.minX, minX); 184 | minZ = std::min(other.minZ, minZ); 185 | maxX = std::max(other.maxX, maxX); 186 | maxZ = std::max(other.maxZ, maxZ); 187 | minY = std::min(other.minY, minY); 188 | maxY = std::max(other.maxY, maxY); 189 | 190 | return *this; 191 | } 192 | 193 | template ::value, int> = 0> 195 | bool operator==(const Coordinates &other) const { 196 | return (minX == other.minX && minZ == other.minZ && maxX == other.maxX && 197 | maxZ == other.maxZ && minY == other.minY && maxY == other.maxY && 198 | orientation == other.orientation); 199 | } 200 | }; 201 | 202 | } // namespace Map 203 | 204 | namespace World { 205 | using Coordinates = Map::Coordinates; 206 | } 207 | -------------------------------------------------------------------------------- /src/include/nbt/iterators.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./tag_types.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace nbt { 11 | 12 | class primitive_iterator_t { 13 | private: 14 | using difference_type = std::ptrdiff_t; 15 | static constexpr difference_type begin_value = 0; 16 | static constexpr difference_type end_value = begin_value + 1; 17 | difference_type m_it = (std::numeric_limits::min)(); 18 | 19 | public: 20 | constexpr difference_type get_value() const noexcept { return m_it; } 21 | void set_begin() noexcept { m_it = begin_value; } 22 | void set_end() noexcept { m_it = end_value; } 23 | constexpr bool is_begin() const noexcept { return m_it == begin_value; } 24 | constexpr bool is_end() const noexcept { return m_it == end_value; } 25 | friend constexpr bool operator==(primitive_iterator_t lhs, 26 | primitive_iterator_t rhs) noexcept { 27 | return lhs.m_it == rhs.m_it; 28 | } 29 | 30 | friend constexpr bool operator<(primitive_iterator_t lhs, 31 | primitive_iterator_t rhs) noexcept { 32 | return lhs.m_it < rhs.m_it; 33 | } 34 | 35 | primitive_iterator_t operator+(difference_type n) noexcept { 36 | auto result = *this; 37 | result += n; 38 | return result; 39 | } 40 | 41 | friend constexpr difference_type 42 | operator-(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept { 43 | return lhs.m_it - rhs.m_it; 44 | } 45 | 46 | primitive_iterator_t &operator++() noexcept { 47 | ++m_it; 48 | return *this; 49 | } 50 | 51 | primitive_iterator_t const operator++(int) noexcept { 52 | auto result = *this; 53 | ++m_it; 54 | return result; 55 | } 56 | 57 | primitive_iterator_t &operator--() noexcept { 58 | --m_it; 59 | return *this; 60 | } 61 | 62 | primitive_iterator_t const operator--(int) noexcept { 63 | auto result = *this; 64 | --m_it; 65 | return result; 66 | } 67 | 68 | primitive_iterator_t &operator+=(difference_type n) noexcept { 69 | m_it += n; 70 | return *this; 71 | } 72 | 73 | primitive_iterator_t &operator-=(difference_type n) noexcept { 74 | m_it -= n; 75 | return *this; 76 | } 77 | }; 78 | 79 | template struct internal_iterator { 80 | typename NBTType::tag_compound_t::iterator compound_iterator{}; 81 | typename NBTType::tag_list_t::iterator list_iterator{}; 82 | primitive_iterator_t primitive_iterator{}; 83 | }; 84 | 85 | template struct WhichType; 86 | 87 | template class iter { 88 | friend iter::value, typename std::remove_const::type, 90 | const NBTType>::type>; 91 | 92 | friend NBTType; 93 | 94 | using compound_t = typename NBTType::tag_compound_t; 95 | using list_t = typename NBTType::tag_list_t; 96 | 97 | public: 98 | using value_type = typename NBTType::value_type; 99 | using difference_type = typename NBTType::difference_type; 100 | using pointer = typename std::conditional::value, 101 | typename NBTType::const_pointer, 102 | typename NBTType::pointer>::type; 103 | using reference = 104 | typename std::conditional::value, 105 | typename NBTType::const_reference, 106 | typename NBTType::reference>::type; 107 | 108 | iter() = default; 109 | 110 | explicit iter(pointer object) noexcept : content(object) { 111 | assert(content != nullptr); 112 | 113 | switch (content->type) { 114 | case tag_type::tag_compound: 115 | it.compound_iterator = typename compound_t::iterator(); 116 | break; 117 | case tag_type::tag_list: 118 | it.list_iterator = typename list_t::iterator(); 119 | break; 120 | default: 121 | it.primitive_iterator = primitive_iterator_t(); 122 | break; 123 | } 124 | } 125 | 126 | iter(const iter &other) noexcept 127 | : content(other.content), it(other.it) {} 128 | 129 | iter &operator=(const iter &other) noexcept { 130 | content = other.content; 131 | it = other.it; 132 | return *this; 133 | } 134 | 135 | iter(const iter::type> &other) noexcept 136 | : content(other.content), it(other.it) {} 137 | 138 | iter &operator=( 139 | const iter::type> &other) noexcept { 140 | content = other.content; 141 | it = other.it; 142 | return *this; 143 | } 144 | 145 | private: 146 | void set_begin() noexcept { 147 | assert(content != nullptr); 148 | 149 | switch (content->type) { 150 | case tag_type::tag_compound: 151 | it.compound_iterator = content->content.compound->begin(); 152 | break; 153 | case tag_type::tag_list: 154 | it.list_iterator = content->content.list->begin(); 155 | break; 156 | case tag_type::tag_end: 157 | // set to end so begin()==end() is true: end is empty 158 | it.primitive_iterator.set_end(); 159 | break; 160 | default: 161 | it.primitive_iterator.set_begin(); 162 | break; 163 | } 164 | } 165 | 166 | void set_end() noexcept { 167 | assert(content != nullptr); 168 | 169 | switch (content->type) { 170 | case tag_type::tag_compound: 171 | it.compound_iterator = content->content.compound->end(); 172 | break; 173 | case tag_type::tag_list: 174 | it.list_iterator = content->content.list->end(); 175 | break; 176 | default: 177 | it.primitive_iterator.set_end(); 178 | break; 179 | } 180 | } 181 | 182 | public: 183 | reference operator*() const { 184 | assert(content != nullptr); 185 | 186 | switch (content->type) { 187 | case tag_type::tag_compound: 188 | assert(it.compound_iterator != content->content.compound->end()); 189 | return it.compound_iterator->second; 190 | case tag_type::tag_list: 191 | assert(it.list_iterator != content->content.list->end()); 192 | return *it.list_iterator; 193 | case tag_type::tag_end: 194 | throw(std::range_error("No values in end tag")); 195 | default: 196 | if (it.primitive_iterator.is_begin()) 197 | return *content; 198 | throw(std::range_error("Cannot get value")); 199 | } 200 | } 201 | 202 | pointer operator->() const { 203 | assert(content != nullptr); 204 | 205 | switch (content->type) { 206 | case tag_type::tag_compound: 207 | assert(it.compound_iterator != content->content.compound->end()); 208 | return &(it.compound_iterator->second); 209 | case tag_type::tag_list: 210 | assert(it.list_iterator != content->content.list->end()); 211 | return &*it.list_iterator; 212 | case tag_type::tag_end: 213 | throw(std::range_error("No values in end tag")); 214 | default: 215 | if (it.primitive_iterator.is_begin()) 216 | return content; 217 | throw(std::range_error("Cannot get value")); 218 | } 219 | } 220 | 221 | iter const operator++(int) { 222 | auto result = *this; 223 | ++(*this); 224 | return result; 225 | } 226 | 227 | iter &operator++() { 228 | assert(content != nullptr); 229 | 230 | switch (content->type) { 231 | case tag_type::tag_compound: 232 | std::advance(it.compound_iterator, 1); 233 | break; 234 | case tag_type::tag_list: 235 | std::advance(it.list_iterator, 1); 236 | break; 237 | default: 238 | ++it.primitive_iterator; 239 | break; 240 | } 241 | 242 | return *this; 243 | } 244 | 245 | iter const operator--(int) { 246 | auto result = *this; 247 | --(*this); 248 | return result; 249 | } 250 | 251 | iter &operator--() { 252 | assert(content != nullptr); 253 | 254 | switch (content->type) { 255 | case tag_type::tag_compound: 256 | std::advance(it.compound_iterator, -1); 257 | break; 258 | case tag_type::tag_list: 259 | std::advance(it.list_iterator, -1); 260 | break; 261 | default: 262 | ++it.primitive_iterator; 263 | break; 264 | } 265 | 266 | return *this; 267 | } 268 | 269 | bool operator==(const iter &other) const { 270 | if (content != other.content) { 271 | throw(std::domain_error("Bad comparison between iterators of type " + 272 | std::string(content->type_name()) + " and type " + 273 | std::string(content->type_name()))); 274 | } 275 | 276 | assert(content != nullptr); 277 | 278 | switch (content->type) { 279 | case tag_type::tag_compound: 280 | return (it.compound_iterator == other.it.compound_iterator); 281 | case tag_type::tag_list: 282 | return (it.list_iterator == other.it.list_iterator); 283 | default: 284 | return (it.primitive_iterator == other.it.primitive_iterator); 285 | } 286 | } 287 | 288 | bool operator!=(const iter &other) const { return !operator==(other); } 289 | 290 | bool operator<(const iter &other) const { 291 | if (content != other.content) { 292 | throw(std::domain_error("Bad comparison between iterators of type " + 293 | std::string(content->type_name()) + " and type " + 294 | std::string(content->type_name()))); 295 | } 296 | 297 | assert(content != nullptr); 298 | 299 | switch (content->type) { 300 | case tag_type::tag_compound: 301 | throw(std::domain_error("Cannot compare compound types")); 302 | case tag_type::tag_list: 303 | return (it.list_iterator < other.it.list_iterator); 304 | default: 305 | return (it.primitive_iterator < other.it.primitive_iterator); 306 | } 307 | } 308 | 309 | bool operator<=(const iter &other) const { 310 | return not other.operator<(*this); 311 | } 312 | 313 | bool operator>(const iter &other) const { return not operator<=(other); } 314 | bool operator>=(const iter &other) const { return not operator<(other); } 315 | 316 | iter &operator+=(difference_type i) { 317 | assert(content != nullptr); 318 | 319 | switch (content->type) { 320 | case tag_type::tag_compound: 321 | throw(std::domain_error("cannot use offsets with compound iterators")); 322 | case tag_type::tag_list: 323 | std::advance(it.list_iterator, i); 324 | break; 325 | default: { 326 | it.primitive_iterator += i; 327 | break; 328 | } 329 | } 330 | 331 | return *this; 332 | } 333 | 334 | iter &operator-=(difference_type i) { return operator+=(-i); } 335 | 336 | iter operator+(difference_type i) const { 337 | auto result = *this; 338 | result += i; 339 | return result; 340 | } 341 | 342 | friend iter operator+(difference_type i, const iter &arg_it) { 343 | auto result = arg_it; 344 | result += i; 345 | return result; 346 | } 347 | 348 | iter operator-(difference_type i) const { 349 | auto result = *this; 350 | result -= i; 351 | return result; 352 | } 353 | 354 | difference_type operator-(const iter &other) const { 355 | assert(content != nullptr); 356 | 357 | switch (content->type) { 358 | case tag_type::tag_compound: 359 | throw(std::domain_error("cannot use offsets with compound iterators")); 360 | case tag_type::tag_list: 361 | return (it.list_iterator - other.it.list_iterator); 362 | default: 363 | return it.primitive_iterator - other.it.primitive_iterator; 364 | } 365 | } 366 | 367 | reference operator[](difference_type n) const { 368 | assert(content != nullptr); 369 | 370 | switch (content->type) { 371 | case tag_type::tag_compound: 372 | throw(std::domain_error("cannot use operator[] with compound iterators")); 373 | case tag_type::tag_list: 374 | return *std::next(it.list_iterator, n); 375 | case tag_type::tag_end: 376 | throw(std::domain_error("cannot get value from end tag")); 377 | 378 | default: { 379 | if (it.primitive_iterator.get_value() == -n) { 380 | return *content; 381 | } 382 | throw(std::domain_error("cannot get value from end tag")); 383 | } 384 | } 385 | } 386 | 387 | const std::string &key() const { 388 | assert(content != nullptr); 389 | if (content->is_compound()) { 390 | return it.compound_iterator->first; 391 | } 392 | throw(std::domain_error( 393 | "cannot use operator key with non-compound iterators")); 394 | } 395 | 396 | reference value() const { return operator*(); } 397 | 398 | private: 399 | pointer content = nullptr; 400 | internal_iterator::type> it{}; 401 | }; 402 | 403 | } // namespace nbt 404 | -------------------------------------------------------------------------------- /src/include/nbt/parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef NBT_GZ_PARSE_HPP_ 3 | #define NBT_GZ_PARSE_HPP_ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Max size of a single element to read in memory (string) 12 | #define MAXELEMENTSIZE 65025 13 | 14 | // Check if the context indicates a being in a list 15 | #define LIST (context.size() && context.top().second < tag_type::tag_long_array) 16 | 17 | namespace fs = std::filesystem; 18 | 19 | namespace nbt { 20 | 21 | static bool format_check(io::ByteStreamReader &b) { 22 | // Check the byte stream begins with a non-end tag and contains a valid 23 | // UTF-8 name 24 | uint8_t buffer[MAXELEMENTSIZE]; 25 | uint16_t name_length = 0; 26 | bool error = false; 27 | 28 | b.read(1, buffer, &error); 29 | if (error || !buffer[0] || buffer[0] > 13) { 30 | logger::trace("NBT format check error: Invalid type read"); 31 | return false; 32 | } 33 | 34 | b.read(2, buffer, &error); 35 | if (error) { 36 | logger::trace("NBT format check error: Invalid name size read"); 37 | return false; 38 | } 39 | 40 | name_length = translate(buffer); 41 | b.read(name_length, buffer, &error); 42 | if (error) { 43 | logger::trace("NBT format check error: Invalid name read"); 44 | return false; 45 | } 46 | 47 | for (uint16_t i = 0; i < name_length; i++) { 48 | if (buffer[i] < 0x21 || buffer[i] > 0x7e) { 49 | logger::trace("NBT format check error: Invalid character read: {:02x}", 50 | buffer[i]); 51 | return false; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | static bool matryoshka(io::ByteStreamReader &b, NBT &destination) { 59 | bool error = false; 60 | 61 | uint8_t buffer[MAXELEMENTSIZE]; 62 | 63 | NBT current; 64 | tag_type current_type = tag_type::tag_end, list_type; 65 | 66 | std::string current_name; 67 | uint32_t elements, list_elements, name_size; 68 | 69 | // The stack with open containers. When a container is found, it is pushed 70 | // on this stack; when an element is finished, it is pushed in the container 71 | // on top of the stack, or returned if the stack is empty. 72 | std::stack opened_elements = {}; 73 | 74 | // The context stack. All was well until the NBT lists came along that 75 | // changed the element format (no type/name). This stack tracks every 76 | // element on the stack. Its contents follow the format: If content left > 0, it is assumed to 78 | // be a NBT list. This changes the behaviour of the parser accordingly. When 79 | // pushing/popping containers, the second element gets the type of the 80 | // current list. 81 | std::stack> context = {}; 82 | 83 | do { 84 | current_name = ""; 85 | 86 | // Get the type, if not in a list 87 | if (!LIST) { 88 | b.read(1, buffer, &error); 89 | current_type = tag_type(buffer[0]); 90 | 91 | } else { 92 | // Grab the type from the list's context 93 | current_type = context.top().second; 94 | } 95 | 96 | // If the tag has a possible name, parse it 97 | if (!LIST && current_type != tag_type::tag_end) { 98 | b.read(2, buffer, &error); 99 | name_size = translate(buffer); 100 | 101 | if (name_size) { 102 | b.read(name_size, buffer, &error); 103 | 104 | current_name = std::string((char *)buffer, name_size); 105 | } 106 | } 107 | 108 | // If end tag -> Close the last compound 109 | if (current_type == tag_type::tag_end || 110 | (LIST && context.top().first == 0)) { 111 | // Grab the container from the stack 112 | current = std::move(opened_elements.top()); 113 | opened_elements.pop(); 114 | 115 | // Remove its context 116 | context.pop(); 117 | 118 | // Continue to the end to merge the container to the previous element of 119 | // the stack 120 | current_type = tag_type::tag_end; 121 | } 122 | 123 | // Compound tag -> Open a compound 124 | if (current_type == tag_type::tag_compound) { 125 | // Push an empty compound on the stack 126 | opened_elements.push(NBT(NBT::tag_compound_t(), current_name)); 127 | 128 | // Add a context 129 | context.push({0, tag_type(0xff)}); 130 | 131 | // Start again 132 | continue; 133 | } 134 | 135 | // List tag -> Read type and length 136 | if (current_type == tag_type::tag_list) { 137 | // Grab the type 138 | b.read(1, buffer, &error); 139 | list_type = nbt::tag_type(buffer[0]); 140 | 141 | // Grab the length 142 | b.read(4, buffer, &error); 143 | list_elements = translate(buffer); 144 | 145 | // Push an empty list on the stack 146 | opened_elements.push(NBT(NBT::tag_list_t(), current_name)); 147 | 148 | // Add a context 149 | context.push({list_elements, list_type}); 150 | 151 | // Start again 152 | continue; 153 | } 154 | 155 | switch (current_type) { 156 | // Handled previously 157 | case tag_type::tag_list: 158 | case tag_type::tag_compound: 159 | case tag_type::tag_end: 160 | break; 161 | 162 | case tag_type::tag_byte: { 163 | // Byte -> Read name and a byte 164 | b.read(1, buffer, &error); 165 | uint8_t byte = buffer[0]; 166 | 167 | current = NBT(byte, current_name); 168 | break; 169 | } 170 | 171 | case tag_type::tag_short: { 172 | b.read(2, buffer, &error); 173 | 174 | current = NBT(translate(buffer), current_name); 175 | break; 176 | } 177 | 178 | case tag_type::tag_int: { 179 | b.read(4, buffer, &error); 180 | 181 | current = NBT(translate(buffer), current_name); 182 | break; 183 | } 184 | 185 | case tag_type::tag_long: { 186 | b.read(8, buffer, &error); 187 | 188 | current = NBT(translate(buffer), current_name); 189 | break; 190 | } 191 | 192 | case tag_type::tag_float: { 193 | b.read(4, buffer, &error); 194 | 195 | current = NBT(translate(buffer), current_name); 196 | break; 197 | } 198 | 199 | case tag_type::tag_double: { 200 | b.read(8, buffer, &error); 201 | 202 | current = NBT(translate(buffer), current_name); 203 | break; 204 | } 205 | 206 | case tag_type::tag_byte_array: { 207 | b.read(4, buffer, &error); 208 | elements = translate(buffer); 209 | 210 | std::vector bytes(elements); 211 | 212 | for (uint32_t i = 0; i < elements; i++) { 213 | b.read(1, buffer, &error); 214 | bytes[i] = buffer[0]; 215 | } 216 | 217 | current = NBT(bytes, current_name); 218 | break; 219 | } 220 | 221 | case tag_type::tag_int_array: { 222 | b.read(4, buffer, &error); 223 | elements = translate(buffer); 224 | 225 | std::vector ints(elements); 226 | 227 | for (uint32_t i = 0; i < elements; i++) { 228 | b.read(4, buffer, &error); 229 | ints[i] = translate(buffer); 230 | } 231 | 232 | current = NBT(ints, current_name); 233 | break; 234 | } 235 | 236 | case tag_type::tag_long_array: { 237 | b.read(4, buffer, &error); 238 | elements = translate(buffer); 239 | 240 | std::vector longs(elements); 241 | for (uint32_t i = 0; i < elements; i++) { 242 | b.read(8, buffer, &error); 243 | longs[i] = translate(buffer); 244 | } 245 | 246 | current = NBT(longs, current_name); 247 | break; 248 | } 249 | 250 | case tag_type::tag_string: { 251 | b.read(2, buffer, &error); 252 | uint16_t string_size = translate(buffer); 253 | 254 | b.read(string_size, buffer, &error); 255 | std::string content((char *)buffer, string_size); 256 | 257 | current = NBT(std::move(content), current_name); 258 | break; 259 | } 260 | } 261 | 262 | // If not in a list 263 | if (!LIST) { 264 | // Add the current element to the previous compound 265 | if (opened_elements.size()) 266 | opened_elements.top()[current.get_name()] = std::move(current); 267 | 268 | } else { 269 | // We in a list 270 | // Grab the array 271 | NBT::tag_list_t *array = opened_elements.top().get(); 272 | array->insert(array->end(), std::move(current)); 273 | 274 | // Decrement the element counter 275 | context.top().first = std::max(uint32_t(0), context.top().first - 1); 276 | } 277 | } while (!error && opened_elements.size()); 278 | 279 | destination = std::move(current); 280 | return !error; 281 | } 282 | 283 | template ::value, 285 | int>::type = 0> 286 | static bool assert_NBT(const fs::path &file) { 287 | gzFile f; 288 | bool status = false; 289 | 290 | if ((f = gzopen(file.string().c_str(), "rb"))) { 291 | io::ByteStreamReader gz(f); 292 | status = format_check(gz); 293 | 294 | gzclose(f); 295 | } else { 296 | logger::error("Error opening file '{}': {}", file.string(), 297 | strerror(errno)); 298 | } 299 | 300 | return status; 301 | } 302 | 303 | template ::value, 305 | int>::type = 0> 306 | static bool assert_NBT(uint8_t *buffer, size_t size) { 307 | bool status = false; 308 | 309 | io::ByteStreamReader mem(buffer, size); 310 | status = format_check(mem); 311 | 312 | return status; 313 | } 314 | 315 | // This completely useless template gets rid of "Function defined but never 316 | // used" warnings. 317 | template < 318 | typename NBT_Type = NBT, 319 | typename std::enable_if::value, int>::type = 0> 320 | static bool parse(const fs::path &file, NBT &container) { 321 | gzFile f; 322 | bool status = false; 323 | 324 | if ((f = gzopen(file.string().c_str(), "rb"))) { 325 | io::ByteStreamReader gz(f); 326 | status = matryoshka(gz, container); 327 | if (!status) 328 | logger::error("Error reading file {}", file.string()); 329 | 330 | gzclose(f); 331 | } else { 332 | logger::error("Error opening file '{}': {}", file.string(), 333 | strerror(errno)); 334 | } 335 | 336 | return status; 337 | } 338 | 339 | template < 340 | typename NBT_Type = NBT, 341 | typename std::enable_if::value, int>::type = 0> 342 | static bool parse(uint8_t *buffer, size_t size, NBT &container) { 343 | bool status = false; 344 | 345 | io::ByteStreamReader mem(buffer, size); 346 | status = matryoshka(mem, container); 347 | if (!status) 348 | logger::error("Error reading NBT data"); 349 | 350 | return status; 351 | } 352 | } // namespace nbt 353 | 354 | #endif 355 | -------------------------------------------------------------------------------- /src/include/nbt/stream.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace nbt { 9 | 10 | namespace io { 11 | struct ByteStream { 12 | // Adapter for the matryoshkas to work with both files and memory buffers 13 | enum BufferType { MEMORY, GZFILE }; 14 | 15 | union Source { 16 | gzFile file; 17 | std::pair array; 18 | 19 | Source(gzFile f) : file(f){}; 20 | Source(uint8_t *address, size_t size) : array({address, size}){}; 21 | }; 22 | 23 | BufferType type; 24 | Source source; 25 | 26 | ByteStream(gzFile f) : type(GZFILE), source(f){}; 27 | ByteStream(uint8_t *address, size_t size) 28 | : type(MEMORY), source(address, size){}; 29 | }; 30 | 31 | struct ByteStreamReader : ByteStream { 32 | ByteStreamReader(gzFile f) : ByteStream(f){}; 33 | ByteStreamReader(uint8_t *address, size_t size) : ByteStream(address, size){}; 34 | 35 | void read(size_t num, uint8_t *buffer, bool *error) { 36 | switch (type) { 37 | case GZFILE: { 38 | if (size_t(gzread(source.file, buffer, num)) < num) { 39 | logger::error("Unexpected EOF"); 40 | *error = true; 41 | memset(buffer, 0, num); 42 | } 43 | 44 | break; 45 | } 46 | 47 | case MEMORY: { 48 | if (source.array.second < num) { 49 | logger::error("Not enough data in memory buffer"); 50 | *error = true; 51 | memset(buffer, 0, num); 52 | } 53 | 54 | memcpy(buffer, source.array.first, num); 55 | source.array.first += num; 56 | source.array.second -= num; 57 | 58 | break; 59 | } 60 | } 61 | } 62 | }; 63 | 64 | struct ByteStreamWriter : ByteStream { 65 | ByteStreamWriter(gzFile f) : ByteStream(f){}; 66 | ByteStreamWriter(uint8_t *address, size_t size) : ByteStream(address, size){}; 67 | 68 | void write(size_t num, const uint8_t *buffer, bool *error) { 69 | switch (type) { 70 | case GZFILE: { 71 | if (size_t(gzwrite(source.file, buffer, num)) < num) { 72 | logger::error("Write error: not enough bytes written"); 73 | *error = true; 74 | } 75 | 76 | break; 77 | } 78 | 79 | case MEMORY: { 80 | if (source.array.second < num) { 81 | logger::error("Not enough space left in memory buffer"); 82 | *error = true; 83 | } 84 | 85 | memcpy(source.array.first, buffer, num); 86 | source.array.first += num; 87 | source.array.second -= num; 88 | 89 | break; 90 | } 91 | } 92 | } 93 | }; 94 | 95 | } // namespace io 96 | 97 | } // namespace nbt 98 | -------------------------------------------------------------------------------- /src/include/nbt/tag_types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace nbt { 5 | 6 | enum class tag_type : uint8_t { 7 | tag_end = 0, 8 | tag_byte, 9 | tag_short, 10 | tag_int, 11 | tag_long, 12 | tag_float, 13 | tag_double, 14 | tag_byte_array, 15 | tag_string = 8, 16 | tag_list, 17 | tag_compound = 10, 18 | tag_int_array, 19 | tag_long_array, 20 | }; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/include/nbt/to_json.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef NBT_TO_JSON_HPP_ 3 | #define NBT_TO_JSON_HPP_ 4 | 5 | #include 6 | #include 7 | 8 | using nlohmann::json; 9 | 10 | namespace nbt { 11 | 12 | void to_json(json &j, const NBT &nbt) { 13 | 14 | switch (nbt.get_type()) { 15 | case tag_type::tag_byte: 16 | case tag_type::tag_short: 17 | case tag_type::tag_int: 18 | case tag_type::tag_long: 19 | j = json({{nbt.get_name(), nbt.get()}}); 20 | break; 21 | case tag_type::tag_float: 22 | case tag_type::tag_double: 23 | j = json({{nbt.get_name(), nbt.get()}}); 24 | break; 25 | case tag_type::tag_byte_array: 26 | j = json({{nbt.get_name(), *nbt.get()}}); 27 | break; 28 | case tag_type::tag_string: 29 | j = json({{nbt.get_name(), nbt.get()}}); 30 | break; 31 | case tag_type::tag_list: { 32 | std::vector data; 33 | const std::vector *subs = nbt.get(); 34 | 35 | for (auto el : *subs) 36 | data.emplace_back(json(el)); 37 | 38 | j = json({{nbt.get_name(), data}}); 39 | break; 40 | } 41 | case tag_type::tag_compound: { 42 | json contents({}); 43 | 44 | const std::map *subs = 45 | nbt.get(); 46 | 47 | for (auto el : *subs) 48 | contents.update(json(el.second)); 49 | 50 | j = json({{nbt.get_name(), contents}}); 51 | break; 52 | } 53 | case tag_type::tag_int_array: 54 | j = json({{nbt.get_name(), *nbt.get()}}); 55 | break; 56 | case tag_type::tag_long_array: 57 | j = json({{nbt.get_name(), *nbt.get()}}); 58 | break; 59 | case tag_type::tag_end: 60 | default: 61 | j = json({}); 62 | break; 63 | } 64 | } 65 | 66 | } // namespace nbt 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /src/include/nbt/writer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef NBT_GZ_WRITE_HPP_ 3 | #define NBT_GZ_WRITE_HPP_ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace nbt { 12 | 13 | bool put(io::ByteStreamWriter &output, const NBT &data) { 14 | bool error = false; 15 | uint8_t buffer[65025]; 16 | 17 | std::stack> parsing; 18 | 19 | parsing.push({data, data.begin()}); 20 | 21 | while (!error && !parsing.empty()) { 22 | const NBT ¤t = parsing.top().first; 23 | NBT::const_iterator position = parsing.top().second; 24 | 25 | parsing.pop(); 26 | 27 | if (position == current.begin() && 28 | current.get_type() != nbt::tag_type::tag_end && 29 | !(!parsing.empty() && 30 | parsing.top().first.get_type() == nbt::tag_type::tag_list)) { 31 | buffer[0] = static_cast(current.get_type()); 32 | output.write(1, buffer, &error); 33 | 34 | uint16_t name_size = static_cast(current.get_name().size()); 35 | // TODO output.write(2, Translator(name_size).bytes(), &error); 36 | 37 | output.write(name_size, (uint8_t *)current.get_name().c_str(), &error); 38 | } 39 | 40 | switch (current.get_type()) { 41 | case nbt::tag_type::tag_end: 42 | break; 43 | 44 | case nbt::tag_type::tag_compound: 45 | if (position == current.end()) { 46 | buffer[0] = static_cast(nbt::tag_type::tag_end); 47 | output.write(1, buffer, &error); 48 | } else { 49 | const NBT &next = *position++; 50 | parsing.push({current, position}); 51 | parsing.push({next, next.begin()}); 52 | } 53 | break; 54 | 55 | case nbt::tag_type::tag_list: 56 | if (position == current.begin()) { 57 | if (position == current.end()) 58 | buffer[0] = static_cast(nbt::tag_type::tag_end); 59 | else 60 | buffer[0] = static_cast(position->get_type()); 61 | output.write(1, buffer, &error); 62 | 63 | uint32_t size = current.size(); 64 | // TODO output.write(4, Translator(size).bytes(), &error); 65 | } 66 | 67 | if (position != current.end()) { 68 | const NBT &next = *position++; 69 | parsing.push({current, position}); 70 | parsing.push({next, next.begin()}); 71 | } 72 | break; 73 | 74 | case nbt::tag_type::tag_byte: 75 | buffer[0] = current.get(); 76 | output.write(1, buffer, &error); 77 | break; 78 | 79 | case nbt::tag_type::tag_short: { 80 | uint16_t value = current.get(); 81 | // TODO output.write(2, Translator(value).bytes(), &error); 82 | break; 83 | } 84 | 85 | case nbt::tag_type::tag_int: { 86 | uint32_t value = current.get(); 87 | // TODO output.write(4, Translator(value).bytes(), &error); 88 | break; 89 | } 90 | 91 | case nbt::tag_type::tag_long: { 92 | uint64_t value = current.get(); 93 | // TODO output.write(8, Translator(value).bytes(), &error); 94 | break; 95 | } 96 | 97 | case nbt::tag_type::tag_float: { 98 | float value = current.get(); 99 | // TODO output.write(4, Translator(value).bytes(), &error); 100 | break; 101 | } 102 | 103 | case nbt::tag_type::tag_double: { 104 | double value = current.get(); 105 | // TODO output.write(8, Translator(value).bytes(), &error); 106 | break; 107 | } 108 | 109 | case nbt::tag_type::tag_byte_array: { 110 | auto values = current.get(); 111 | uint32_t size = values->size(); 112 | // TODO output.write(4, Translator(size).bytes(), &error); 113 | 114 | for (int8_t value : *values) { 115 | buffer[0] = value; 116 | output.write(1, buffer, &error); 117 | } 118 | break; 119 | } 120 | 121 | case nbt::tag_type::tag_int_array: { 122 | auto values = current.get(); 123 | uint32_t size = values->size(); 124 | // TODO output.write(4, Translator(size).bytes(), &error); 125 | 126 | for (int32_t value : *values) 127 | // TODO output.write(4, Translator(value).bytes(), &error); 128 | 129 | break; 130 | } 131 | 132 | case nbt::tag_type::tag_long_array: { 133 | auto values = current.get(); 134 | uint32_t size = values->size(); 135 | // TODO output.write(4, Translator(size).bytes(), &error); 136 | 137 | for (int64_t value : *values) { 138 | // TODO output.write(8, Translator(value).bytes(), &error); 139 | } 140 | break; 141 | } 142 | 143 | case nbt::tag_type::tag_string: { 144 | auto value = current.get(); 145 | uint16_t size = static_cast(value.size()); 146 | // TODO output.write(2, Translator(size).bytes(), &error); 147 | 148 | output.write(size, (uint8_t *)value.c_str(), &error); 149 | break; 150 | } 151 | } 152 | } 153 | 154 | return error; 155 | } 156 | 157 | } // namespace nbt 158 | 159 | #endif 160 | -------------------------------------------------------------------------------- /src/include/progress.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef _WINDOWS 10 | // TTY support on linux 11 | #include 12 | #endif 13 | 14 | namespace Progress { 15 | 16 | enum Action { 17 | RENDERING = 0, 18 | COMPOSING, 19 | TILING, 20 | }; 21 | 22 | typedef std::function Callback; 23 | 24 | const std::map action_strings = { 25 | {Progress::RENDERING, "Rendering terrain"}, 26 | {Progress::COMPOSING, "Composing final image"}, 27 | {Progress::TILING, "Splitting image in tiles"}, 28 | }; 29 | 30 | struct Status { 31 | 32 | static void quiet(int, int, int){}; 33 | static void ascii(int d, int t, Progress::Action status) { 34 | #ifndef _WINDOWS 35 | // Only print progress bar if stderr is a tty 36 | if (!isatty(STDERR_FILENO)) 37 | return; 38 | #endif 39 | 40 | fmt::print(stderr, "\r{} [{:.{}f}%]\r", action_strings.at(status), 41 | float(d) / float(t) * 100.0f, 2); 42 | fflush(stderr); 43 | 44 | if (d == t) 45 | fmt::print(stderr, "\r \r"); 46 | }; 47 | 48 | Callback notify; 49 | size_t done, total; 50 | Progress::Action type; 51 | 52 | Status(size_t total, Callback cb = quiet, 53 | Progress::Action action = Progress::RENDERING) 54 | : done(0), total(total), type(action) { 55 | notify = cb; 56 | 57 | notify(0, total, type); 58 | } 59 | 60 | void increment(size_t value = 1) { 61 | done = std::min(done + value, total); 62 | notify(done, total, type); 63 | } 64 | }; 65 | 66 | } // namespace Progress 67 | -------------------------------------------------------------------------------- /src/include/translator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // enum DataType { SHORT, INT, LONG, FLOAT, DOUBLE }; 7 | 8 | inline void swap16(const uint8_t *src, uint8_t *dest) { 9 | dest[0] = src[1]; 10 | dest[1] = src[0]; 11 | } 12 | 13 | inline void swap32(const uint8_t *src, uint8_t *dest) { 14 | dest[0] = src[3]; 15 | dest[1] = src[2]; 16 | dest[2] = src[1]; 17 | dest[3] = src[0]; 18 | } 19 | 20 | inline void swap64(const uint8_t *src, uint8_t *dest) { 21 | dest[0] = src[7]; 22 | dest[1] = src[6]; 23 | dest[2] = src[5]; 24 | dest[3] = src[4]; 25 | dest[4] = src[3]; 26 | dest[5] = src[2]; 27 | dest[6] = src[1]; 28 | dest[7] = src[0]; 29 | } 30 | 31 | template ::value, 33 | int>::type = 0> 34 | ArithmeticType translate(const uint8_t *_bytes) { 35 | uint8_t buffer[8]; 36 | std::size_t bytes_n = std::alignment_of(); 37 | 38 | switch (bytes_n) { 39 | case 2: 40 | swap16(_bytes, buffer); 41 | break; 42 | 43 | case 4: 44 | swap32(_bytes, buffer); 45 | break; 46 | 47 | case 8: 48 | swap64(_bytes, buffer); 49 | break; 50 | } 51 | 52 | return *reinterpret_cast(buffer); 53 | } 54 | -------------------------------------------------------------------------------- /src/mcmap.cpp: -------------------------------------------------------------------------------- 1 | #include "./mcmap.h" 2 | #include 3 | 4 | #ifdef _OPENMP 5 | #define THREADS omp_get_max_threads() 6 | #else 7 | #define THREADS 1 8 | #endif 9 | 10 | namespace mcmap { 11 | 12 | bool writeMapInfo(fs::path outFile, const Canvas &finalImage, 13 | const uint32_t tileSize) { 14 | json data({{"imageDimensions", {finalImage.width(), finalImage.height()}}, 15 | {"layerLocation", outFile.string()}, 16 | {"tileSize", tileSize}}); 17 | 18 | fs::path infoFile = outFile / "mapinfo.json"; 19 | std::ofstream infoStream; 20 | 21 | try { 22 | infoStream.open(infoFile.string()); 23 | } catch (const std::exception &err) { 24 | logger::error("Failed to open {} for writing: {}", infoFile.string(), 25 | err.what()); 26 | return false; 27 | } 28 | 29 | infoStream << data.dump(); 30 | infoStream.close(); 31 | 32 | return true; 33 | } 34 | 35 | int render(const Settings::WorldOptions &options, const Colors::Palette &colors, 36 | Progress::Callback cb) { 37 | logger::debug("Rendering {} with {}", options.save.name, 38 | options.boundaries.to_string()); 39 | 40 | // Divide terrain according to fragment size 41 | std::vector fragment_coordinates; 42 | options.boundaries.fragment(fragment_coordinates, options.fragment_size); 43 | 44 | std::vector fragments(fragment_coordinates.size()); 45 | 46 | Progress::Status s = 47 | Progress::Status(fragment_coordinates.size(), cb, Progress::RENDERING); 48 | 49 | // This value represents the amount of canvasses that can fit in memory at 50 | // once to avoid going over the limit of RAM 51 | Counter capacity = 52 | memory_capacity(options.mem_limit, fragment_coordinates[0].footprint(), 53 | fragment_coordinates.size(), THREADS); 54 | 55 | if (!capacity) 56 | return false; 57 | 58 | logger::debug("Memory capacity: {} fragments - {} fragments scheduled", 59 | size_t(capacity), fragment_coordinates.size()); 60 | 61 | // If caching is needed, ensure the cache directory is available 62 | if (capacity < fragments.size()) 63 | if (!prepare_cache(getTempDir())) 64 | return false; 65 | 66 | auto begin = std::chrono::high_resolution_clock::now(); 67 | #ifdef _OPENMP 68 | #pragma omp parallel shared(fragments, capacity) 69 | #endif 70 | { 71 | #ifdef _OPENMP 72 | #pragma omp for ordered schedule(dynamic) 73 | #endif 74 | for (OMP_FOR_INDEX i = 0; i < fragment_coordinates.size(); i++) { 75 | logger::debug("Rendering {}", fragment_coordinates[i].to_string()); 76 | IsometricCanvas canvas; 77 | canvas.setMap(fragment_coordinates[i]); 78 | canvas.setColors(colors); 79 | 80 | // Load the minecraft terrain to render 81 | Terrain::Data world(fragment_coordinates[i], options.regionDir(), colors); 82 | 83 | // Draw the terrain fragment 84 | canvas.shading = options.shading; 85 | canvas.lighting = options.lighting; 86 | canvas.setMarkers(options.totalMarkers, options.markers); 87 | canvas.renderTerrain(world); 88 | 89 | if (!canvas.empty()) { 90 | if (i >= capacity) { 91 | fs::path temporary = getTempDir() / canvas.map.to_string(); 92 | canvas.save(temporary); 93 | 94 | fragments[i] = std::move(ImageCanvas(canvas.map, temporary)); 95 | } else 96 | fragments[i] = std::move(canvas); 97 | } else { 98 | // If the canvas was empty, increase the capacity to reflect the free 99 | // space 100 | if (i < capacity) 101 | ++capacity; 102 | } 103 | 104 | #ifdef _OPENMP 105 | #pragma omp critical 106 | #endif 107 | { s.increment(); } 108 | } 109 | } 110 | 111 | auto end = std::chrono::high_resolution_clock::now(); 112 | 113 | logger::debug( 114 | "Rendered in {}ms", 115 | std::chrono::duration_cast(end - begin) 116 | .count()); 117 | 118 | CompositeCanvas merged(std::move(fragments)); 119 | logger::debug("{}", merged.to_string()); 120 | 121 | if (merged.empty()) { 122 | logger::error("Canvas is empty !"); 123 | return false; 124 | } 125 | 126 | begin = std::chrono::high_resolution_clock::now(); 127 | 128 | bool save_status; 129 | 130 | if (options.tile_size && 131 | writeMapInfo(options.outFile, merged, options.tile_size)) { 132 | save_status = merged.tile(options.outFile, options.tile_size, cb); 133 | } else { 134 | save_status = merged.save(options.outFile, options.padding, cb); 135 | } 136 | 137 | end = std::chrono::high_resolution_clock::now(); 138 | 139 | if (!save_status) 140 | return false; 141 | 142 | logger::debug( 143 | "Drawn PNG in {}ms", 144 | std::chrono::duration_cast(end - begin) 145 | .count()); 146 | 147 | return true; 148 | } 149 | 150 | std::string version() { 151 | return fmt::format(VERSION " {}bit", 8 * static_cast(sizeof(size_t))); 152 | } 153 | 154 | std::map compilation_options() { 155 | std::map enabled = { 156 | {"Architecture", 157 | fmt::format("{} bits", 8 * static_cast(sizeof(size_t)))}, 158 | #ifdef FMT_VERSION 159 | {"fmt version", 160 | fmt::format("{}.{}.{}", FMT_VERSION / 10000, (FMT_VERSION / 100) % 100, 161 | FMT_VERSION % 100)}, 162 | #endif 163 | #ifdef SPDLOG_VERSION 164 | {"spdlog version", 165 | fmt::format("{}.{}.{}", SPDLOG_VERSION / 10000, 166 | (SPDLOG_VERSION / 100) % 100, SPDLOG_VERSION % 100)}, 167 | #endif 168 | #ifdef PNG_LIBPNG_VER_STRING 169 | {"libpng version", PNG_LIBPNG_VER_STRING}, 170 | #endif 171 | #ifdef ZLIB_VERSION 172 | {"zlib version", ZLIB_VERSION}, 173 | #endif 174 | #ifdef SCM_COMMIT 175 | {"Source version", SCM_COMMIT}, 176 | #endif 177 | #ifdef _OPENMP 178 | {"Threading", "OpenMP"}, 179 | #endif 180 | #ifdef CXX_COMPILER_ID 181 | #ifdef CXX_COMPILER_VERSION 182 | {"Compiler", fmt::format("{} {}", CXX_COMPILER_ID, CXX_COMPILER_VERSION)}, 183 | #endif 184 | #endif 185 | #ifdef DEBUG_BUILD 186 | {"Debug", "Enabled"}, 187 | #endif 188 | }; 189 | 190 | return enabled; 191 | } 192 | 193 | } // namespace mcmap 194 | -------------------------------------------------------------------------------- /src/mcmap.h: -------------------------------------------------------------------------------- 1 | #ifndef MCMAP_H_ 2 | #define MCMAP_H_ 3 | 4 | #include "./VERSION" 5 | #include "./canvas.h" 6 | #include "./settings.h" 7 | #include 8 | #include 9 | 10 | namespace mcmap { 11 | 12 | int render(const Settings::WorldOptions &, const Colors::Palette &, 13 | Progress::Callback = Progress::Status::quiet); 14 | 15 | std::string version(); 16 | 17 | std::map compilation_options(); 18 | 19 | } // namespace mcmap 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/png.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains functions to create and save a png file 3 | */ 4 | 5 | #include "png.h" 6 | #include 7 | 8 | #ifndef Z_BEST_SPEED 9 | #define Z_BEST_SPEED 6 10 | #endif 11 | 12 | namespace PNG { 13 | 14 | PNG::PNG(const std::filesystem::path &file) : file(file), imageHandle(nullptr) { 15 | set_type(UNKNOWN); 16 | _line = _height = _width = _padding = 0; 17 | pngPtr = NULL; 18 | pngInfoPtr = NULL; 19 | } 20 | 21 | void PNG::_close() { 22 | if (imageHandle) { 23 | fclose(imageHandle); 24 | imageHandle = nullptr; 25 | } 26 | 27 | _line = 0; 28 | } 29 | 30 | bool PNG::error_callback() { 31 | // libpng will issue a longjmp on error, so code flow will end up here if 32 | // something goes wrong in the code below 33 | if (setjmp(png_jmpbuf(pngPtr))) { 34 | logger::error("[PNG] libpng encountered an error"); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | ColorType PNG::set_type(int type) { 42 | switch (type) { 43 | case GRAYSCALEALPHA: 44 | _type = GRAYSCALEALPHA; 45 | _bytesPerPixel = 2; 46 | break; 47 | 48 | case GRAYSCALE: 49 | _type = GRAYSCALE; 50 | _bytesPerPixel = 1; 51 | break; 52 | 53 | case PALETTE: 54 | _type = PALETTE; 55 | _bytesPerPixel = 1; 56 | break; 57 | 58 | case RGB: 59 | _type = RGB; 60 | _bytesPerPixel = 3; 61 | break; 62 | 63 | case RGBA: 64 | _type = RGBA; 65 | _bytesPerPixel = 4; 66 | break; 67 | 68 | default: 69 | _type = UNKNOWN; 70 | _bytesPerPixel = 0; 71 | } 72 | 73 | return _type; 74 | } 75 | 76 | PNGWriter::PNGWriter(const std::filesystem::path &file) : super::PNG(file) { 77 | buffer = nullptr; 78 | set_type(RGBA); 79 | } 80 | 81 | PNGWriter::~PNGWriter() { 82 | if (buffer) 83 | delete[] buffer; 84 | } 85 | 86 | void PNGWriter::_close() { 87 | png_write_end(pngPtr, NULL); 88 | png_destroy_write_struct(&pngPtr, &pngInfoPtr); 89 | 90 | super::_close(); 91 | } 92 | 93 | void PNGWriter::set_text(const Comments &_comments) { comments = _comments; } 94 | 95 | void write_text(png_structp pngPtr, png_infop pngInfoPtr, 96 | const Comments &comments) { 97 | std::vector text(comments.size()); 98 | size_t index = 0; 99 | 100 | for (auto const &pair : comments) { 101 | text[index].compression = PNG_TEXT_COMPRESSION_NONE; 102 | text[index].key = (png_charp)pair.first.c_str(); 103 | text[index].text = (png_charp)pair.second.c_str(); 104 | text[index].text_length = pair.second.length(); 105 | 106 | index++; 107 | } 108 | 109 | png_set_text(pngPtr, pngInfoPtr, &text[0], text.size()); 110 | } 111 | 112 | void PNGWriter::_open() { 113 | if (!(get_width() || get_height())) { 114 | logger::warn("[PNGWriter] Nothing to output: canvas is empty !"); 115 | return; 116 | } 117 | 118 | logger::trace("[PNGWriter] image {}x{}, {}bpp, writing to {}", get_width(), 119 | get_height(), 8 * _bytesPerPixel, file.string()); 120 | 121 | if (!(super::imageHandle = fopen(file.string().c_str(), "wb"))) { 122 | logger::error("[PNGWriter] Error opening '{}' for writing: {}", 123 | file.string(), strerror(errno)); 124 | return; 125 | } 126 | 127 | FSEEK(imageHandle, 0, SEEK_SET); 128 | 129 | // Write header 130 | pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 131 | 132 | if (error_callback()) 133 | return; 134 | 135 | if (pngPtr == NULL) { 136 | return; 137 | } 138 | 139 | pngInfoPtr = png_create_info_struct(pngPtr); 140 | 141 | if (pngInfoPtr == NULL) { 142 | png_destroy_write_struct(&pngPtr, NULL); 143 | return; 144 | } 145 | 146 | png_init_io(pngPtr, imageHandle); 147 | 148 | write_text(pngPtr, pngInfoPtr, comments); 149 | 150 | // The png file format works by having blocks piled up in a certain order. 151 | // Check out http://www.libpng.org/pub/png/book/chapter11.html for more 152 | // info. 153 | 154 | // First, dump the required IHDR block. 155 | png_set_IHDR(pngPtr, pngInfoPtr, get_width(), get_height(), 8, 156 | PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, 157 | PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); 158 | 159 | png_write_info(pngPtr, pngInfoPtr); 160 | 161 | return; 162 | } 163 | 164 | void PNGWriter::pad() { 165 | auto blank = new uint8_t[row_size()]; 166 | memset(blank, 0, row_size()); 167 | 168 | for (size_t k = 0; k < _padding; k++) 169 | png_write_row(pngPtr, (png_bytep)buffer); 170 | 171 | delete[] blank; 172 | } 173 | 174 | uint8_t *PNGWriter::getBuffer() { 175 | if (!buffer) { 176 | buffer = new uint8_t[row_size()]; 177 | memset(buffer, 0, row_size()); 178 | } 179 | 180 | return buffer + _padding * _bytesPerPixel; 181 | } 182 | 183 | uint32_t PNGWriter::writeLine() { 184 | if (!_line++) { 185 | _open(); 186 | pad(); 187 | } 188 | 189 | png_write_row(pngPtr, (png_bytep)buffer); 190 | 191 | if (_line == _height) { 192 | pad(); 193 | _close(); 194 | } 195 | 196 | return row_size(); 197 | } 198 | 199 | PNGReader::PNGReader(const std::filesystem::path &file) : PNG(file) { 200 | _open(); 201 | _close(); 202 | } 203 | 204 | PNGReader::PNGReader(const PNGReader &other) : PNG(other.file) {} 205 | 206 | PNGReader::~PNGReader() {} 207 | 208 | void PNGReader::_close() { 209 | png_destroy_read_struct(&pngPtr, &pngInfoPtr, NULL); 210 | super::_close(); 211 | } 212 | 213 | void PNGReader::_open() { 214 | logger::trace("[PNGReader] Opening '{}'", file.string()); 215 | 216 | if (!(super::imageHandle = fopen(file.string().c_str(), "rb"))) { 217 | logger::error("[PNGReader] Error opening '{}' for reading: {}", 218 | file.string(), strerror(errno)); 219 | return; 220 | } 221 | 222 | // Check the validity of the header 223 | png_byte header[8]; 224 | if (fread(header, 1, 8, imageHandle) != 8 || !png_check_sig(header, 8)) { 225 | logger::error("[PNGReader] File '{}' is not a PNG", file.string()); 226 | return; 227 | } 228 | 229 | pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 230 | // Tell libpng the file's header has been handled 231 | png_set_sig_bytes(pngPtr, 8); 232 | 233 | if (pngPtr == NULL || error_callback()) { 234 | logger::error("[PNGReader] Error reading '{}'", file.string()); 235 | return; 236 | } 237 | 238 | pngInfoPtr = png_create_info_struct(pngPtr); 239 | 240 | png_uint_32 width, height; 241 | int type, interlace, comp, filter, _bitDepth; 242 | 243 | png_init_io(pngPtr, imageHandle); 244 | 245 | png_read_info(pngPtr, pngInfoPtr); 246 | 247 | // Check image format (square, RGBA) 248 | png_uint_32 ret = png_get_IHDR(pngPtr, pngInfoPtr, &width, &height, 249 | &_bitDepth, &type, &interlace, &comp, &filter); 250 | if (ret == 0) { 251 | logger::error("[PNGReader] Error getting IDHR block of '{}'", 252 | file.string()); 253 | png_destroy_read_struct(&pngPtr, &pngInfoPtr, NULL); 254 | return; 255 | } 256 | 257 | // Use the gathered info to fill the struct 258 | _width = width; 259 | _height = height; 260 | 261 | set_type(type); 262 | 263 | logger::trace("[PNGReader] '{}': PNG file of size {}x{}, {}bpp", 264 | file.string(), get_width(), get_height(), 8 * _bytesPerPixel); 265 | } 266 | 267 | uint32_t PNGReader::getLine(uint8_t *buffer, size_t size) { 268 | // Open and initialize if this is the first read 269 | if (!_line++) 270 | _open(); 271 | 272 | if (size < get_width()) { 273 | logger::error("[PNGReader] Buffer too small reading '{}' !", file.string()); 274 | return 0; 275 | } 276 | 277 | png_bytep row_pointer = (png_bytep)buffer; 278 | 279 | png_read_row(pngPtr, row_pointer, NULL); 280 | 281 | if (_line == _height) 282 | _close(); 283 | 284 | return get_width(); 285 | } 286 | 287 | } // namespace PNG 288 | -------------------------------------------------------------------------------- /src/png.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAW_PNG_H_ 2 | #define DRAW_PNG_H_ 3 | 4 | #include "./helper.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace PNG { 12 | 13 | typedef std::map Comments; 14 | 15 | enum ColorType { 16 | RGB = PNG_COLOR_TYPE_RGB, 17 | RGBA = PNG_COLOR_TYPE_RGB_ALPHA, 18 | GRAYSCALE = PNG_COLOR_TYPE_GRAY, 19 | GRAYSCALEALPHA = PNG_COLOR_TYPE_GRAY_ALPHA, 20 | PALETTE = PNG_COLOR_TYPE_PALETTE, 21 | UNKNOWN = -1 22 | }; 23 | 24 | struct PNG { 25 | const std::filesystem::path file; 26 | 27 | FILE *imageHandle; 28 | png_structp pngPtr; 29 | png_infop pngInfoPtr; 30 | 31 | ColorType _type; 32 | uint8_t _bytesPerPixel; 33 | uint32_t _width, _height; 34 | uint8_t _padding; 35 | 36 | size_t _line; 37 | 38 | PNG(const std::filesystem::path &file); 39 | PNG(PNG &&other) : file(other.file), imageHandle(nullptr) { 40 | set_type(other._type); 41 | 42 | _line = 0; 43 | _height = other._height; 44 | _width = other._width; 45 | _padding = other._padding; 46 | 47 | pngPtr = NULL; 48 | pngInfoPtr = NULL; 49 | } 50 | ~PNG() { _close(); } 51 | 52 | void _close(); 53 | 54 | ColorType set_type(int); 55 | 56 | void set_padding(uint8_t padding) { _padding = padding; } 57 | 58 | uint32_t get_height() { return _height + 2 * _padding; }; 59 | void set_height(uint32_t height) { _height = height; }; 60 | 61 | uint32_t get_width() { return _width + 2 * _padding; }; 62 | void set_width(uint32_t width) { _width = width; }; 63 | 64 | bool error_callback(); 65 | 66 | inline size_t row_size() { return get_width() * _bytesPerPixel; }; 67 | }; 68 | 69 | struct PNGWriter : public PNG { 70 | Comments comments; 71 | 72 | PNGWriter(const std::filesystem::path &); 73 | PNGWriter(PNGWriter &&other) : PNG(other.file) { 74 | set_type(RGBA); 75 | 76 | _line = other._line; 77 | _height = other._height; 78 | _width = other._width; 79 | _padding = other._padding; 80 | 81 | pngPtr = NULL; 82 | pngInfoPtr = NULL; 83 | 84 | buffer = nullptr; 85 | }; 86 | 87 | ~PNGWriter(); 88 | 89 | void _open(); 90 | void _close(); 91 | 92 | uint8_t *buffer; 93 | 94 | void set_text(const Comments &); 95 | 96 | uint8_t *getBuffer(); 97 | uint32_t writeLine(); 98 | void pad(); 99 | 100 | private: 101 | typedef PNG super; 102 | }; 103 | 104 | struct PNGReader : public PNG { 105 | 106 | PNGReader(const std::filesystem::path &); 107 | PNGReader(const PNGReader &other); 108 | ~PNGReader(); 109 | 110 | void _open(); 111 | void _close(); 112 | 113 | void analyse(); 114 | uint32_t getLine(uint8_t *, size_t); 115 | 116 | private: 117 | typedef PNG super; 118 | }; 119 | 120 | } // namespace PNG 121 | 122 | #endif // DRAW_PNG_H_ 123 | -------------------------------------------------------------------------------- /src/region.cpp: -------------------------------------------------------------------------------- 1 | #include "./region.h" 2 | 3 | uint32_t _ntohi(uint8_t *val) { 4 | return (uint32_t(val[0]) << 24) + (uint32_t(val[1]) << 16) + 5 | (uint32_t(val[2]) << 8) + (uint32_t(val[3])); 6 | } 7 | 8 | std::pair Region::coordinates(const fs::path &_file) { 9 | int16_t rX, rZ; 10 | std::string buffer; 11 | const char delimiter = '.'; 12 | 13 | std::stringstream ss(_file.filename().string()); 14 | std::getline(ss, buffer, delimiter); // This removes the 'r.' 15 | std::getline(ss, buffer, delimiter); // X in r.X.Z.mca 16 | 17 | rX = atoi(buffer.c_str()); 18 | 19 | std::getline(ss, buffer, delimiter); // Z in r.X.Z.mca 20 | 21 | rZ = atoi(buffer.c_str()); 22 | 23 | return {rX, rZ}; 24 | } 25 | 26 | Region::Region(const fs::path &_file) : file(_file) { 27 | locations.fill(Location()); 28 | 29 | std::ifstream regionData(file, std::ifstream::binary); 30 | 31 | for (uint16_t chunk = 0; chunk < REGIONSIZE * REGIONSIZE; chunk++) { 32 | char buffer[4]; 33 | regionData.read(buffer, 4); 34 | 35 | locations[chunk].raw_data = _ntohi((uint8_t *)buffer); 36 | 37 | if (!regionData) { 38 | logger::error("Error reading `{}` header.", _file.string()); 39 | break; 40 | } 41 | } 42 | 43 | regionData.close(); 44 | } 45 | 46 | void Region::write(const fs::path &_file) { 47 | std::ofstream out(_file.c_str(), std::ofstream::binary); 48 | 49 | for (uint16_t chunk = 0; chunk < REGIONSIZE * REGIONSIZE; chunk++) { 50 | uint32_t bytes = _ntohi((uint8_t *)&locations[chunk].raw_data); 51 | out.write((char *)&bytes, 4); 52 | } 53 | 54 | std::array timestamps; 55 | timestamps.fill(0); 56 | 57 | out.write(×tamps[0], 4 * REGIONSIZE * REGIONSIZE); 58 | } 59 | 60 | size_t Region::get_offset(uint8_t max_size) { 61 | // Get an offset at which the first empty 4K bytes blocks are free 62 | std::array sorted = locations; 63 | std::sort(sorted.begin(), sorted.end(), Location::order); 64 | 65 | size_t block = 2; 66 | auto it = sorted.begin(); 67 | 68 | while (it != sorted.end()) { 69 | if (it->offset() != block) { 70 | logger::trace("Empty region of size {} at offset {}", 71 | it->offset() - block, block); 72 | if (it->offset() - block >= max_size) 73 | break; 74 | } 75 | 76 | block = it->offset() + it->size(); 77 | 78 | it++; 79 | } 80 | 81 | return block; 82 | } 83 | -------------------------------------------------------------------------------- /src/region.h: -------------------------------------------------------------------------------- 1 | #include "./helper.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace fs = std::filesystem; 9 | 10 | struct Location { 11 | using offset_t = uint32_t; 12 | using size_t = uint8_t; 13 | 14 | uint32_t raw_data; 15 | 16 | Location() : raw_data(0){}; 17 | 18 | offset_t offset() const { return raw_data >> 8; } 19 | size_t size() const { return raw_data & 0xff; } 20 | 21 | static bool order(const Location &lhs, const Location &rhs) { 22 | return lhs.offset() < rhs.offset(); 23 | } 24 | }; 25 | 26 | struct Region { 27 | static std::pair coordinates(const fs::path &); 28 | 29 | fs::path file; 30 | std::array locations; 31 | 32 | Region() { locations.fill(Location()); } 33 | 34 | Region(const fs::path &); 35 | 36 | void write(const fs::path &); 37 | 38 | size_t get_offset(uint8_t); 39 | }; 40 | 41 | template <> struct fmt::formatter { 42 | char presentation = 'r'; 43 | 44 | constexpr auto parse(format_parse_context &ctx) { 45 | auto it = ctx.begin(), end = ctx.end(); 46 | 47 | if (it != end && *it == 'r') 48 | presentation = *it++; 49 | 50 | // Check if reached the end of the range: 51 | if (it != end && *it != '}') 52 | throw format_error("invalid format"); 53 | 54 | // Return an iterator past the end of the parsed range: 55 | return it; 56 | } 57 | 58 | template 59 | auto format(const Region &r, FormatContext &ctx) { 60 | auto unknown = format_to(ctx.out(), "Contents of {}\n", r.file.string()); 61 | for (int it = 0; it < REGIONSIZE * REGIONSIZE; it++) { 62 | unknown = 63 | format_to(ctx.out(), "{:02d}\t{:02d}\t{:04d}\t{:02d}\n", it & 0x1f, 64 | it >> 5, r.locations[it].offset(), r.locations[it].size()); 65 | } 66 | 67 | return unknown; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/savefile.cpp: -------------------------------------------------------------------------------- 1 | #include "./savefile.h" 2 | 3 | Dimension::Dimension(std::string _id) : ns("minecraft") { 4 | size_t sep = _id.find_first_of(':'); 5 | 6 | if (sep == std::string::npos) { 7 | // If there is no ':', this is just an id 8 | id = _id; 9 | } else { 10 | // If not, add each part separately 11 | ns = _id.substr(0, sep); 12 | id = _id.substr(sep + 1); 13 | } 14 | } 15 | 16 | bool Dimension::operator==(const Dimension &other) const { 17 | return id == other.id && ns == other.ns; 18 | } 19 | 20 | fs::path Dimension::suffix() const { 21 | if (id == "overworld") 22 | return "region"; 23 | else if (id == "the_nether") 24 | return fs::path("DIM-1/region"); 25 | else if (id == "the_end") 26 | return fs::path("DIM1/region"); 27 | else 28 | return fs::path(fmt::format("dimensions/{}/{}/region", ns, id)); 29 | } 30 | 31 | bool assert_save(const fs::path &root) { 32 | fs::path level = root / "level.dat", region = root / "region"; 33 | 34 | std::map requirements = { 35 | {root, fs::file_type::directory}, {region, fs::file_type::directory}}; 36 | 37 | for (auto &r : requirements) { 38 | if (fs::status(r.first).type() != r.second) { 39 | logger::debug("File '{}' is of an unexpected format", r.first.string()); 40 | return false; 41 | } 42 | } 43 | 44 | if (!nbt::assert_NBT(level)) 45 | return false; 46 | 47 | return true; 48 | } 49 | 50 | SaveFile::SaveFile() { last_played = 0; } 51 | 52 | SaveFile::SaveFile(const fs::path &_folder) : folder(_folder) { 53 | nbt::NBT level_data; 54 | 55 | fs::path datafile = _folder / "level.dat"; 56 | 57 | logger::debug("Parsing {}", datafile.string()); 58 | 59 | if (!(nbt::assert_NBT(datafile) && nbt::parse(datafile, level_data))) { 60 | last_played = 0; 61 | return; 62 | } 63 | 64 | name = level_data["Data"]["LevelName"].get(); 65 | last_played = level_data["Data"]["LastPlayed"].get(); 66 | 67 | getDimensions(); 68 | } 69 | 70 | void SaveFile::getDimensions() { 71 | #define VALID(path) fs::exists((path)) && !fs::is_empty((path)) 72 | if (VALID(folder / "region")) 73 | dimensions.push_back(Dimension("overworld")); 74 | 75 | if (VALID(folder / "DIM1/region")) 76 | dimensions.push_back(Dimension("the_end")); 77 | 78 | if (VALID(folder / "DIM-1/region")) 79 | dimensions.push_back(Dimension("the_nether")); 80 | 81 | fs::path dim_folder = folder / "dimensions"; 82 | 83 | if (VALID(dim_folder)) 84 | for (auto &ns : fs::directory_iterator(dim_folder)) 85 | for (auto &id : fs::directory_iterator(ns.path())) 86 | dimensions.push_back(Dimension(ns.path().filename().string(), 87 | id.path().filename().string())); 88 | #undef VALID 89 | } 90 | 91 | fs::path 92 | SaveFile::region(const Dimension &dim = std::string("overworld")) const { 93 | auto found = std::find(dimensions.begin(), dimensions.end(), dim); 94 | 95 | if (found == dimensions.end()) 96 | return ""; 97 | 98 | return folder / dim.suffix(); 99 | } 100 | 101 | void to_json(json &j, const Dimension &d) { 102 | j = fmt::format("{}:{}", d.ns, d.id); 103 | } 104 | 105 | void to_json(json &j, const SaveFile &s) { 106 | j["name"] = s.name; 107 | j["last_played"] = s.last_played; 108 | j["folder"] = s.folder.string(); 109 | j["dimensions"] = s.dimensions; 110 | } 111 | 112 | World::Coordinates SaveFile::getWorld(const Dimension &dim) { 113 | const char delimiter = '.'; 114 | std::string index; 115 | char buffer[4]; 116 | int32_t regionX, regionZ; 117 | 118 | World::Coordinates savedWorld; 119 | savedWorld.setUndefined(); 120 | 121 | if (region(dim).empty()) 122 | return savedWorld; 123 | 124 | for (auto ®ion : fs::directory_iterator(region(dim))) { 125 | // This loop parses files with name 'r.x.y.mca', extracting x and y. This is 126 | // done by creating a string stream and using `getline` with '.' as a 127 | // delimiter. 128 | std::stringstream ss(region.path().filename().string()); 129 | std::getline(ss, index, delimiter); // This removes the 'r.' 130 | std::getline(ss, index, delimiter); 131 | 132 | regionX = atoi(index.c_str()); 133 | 134 | std::getline(ss, index, delimiter); 135 | 136 | regionZ = atoi(index.c_str()); 137 | 138 | std::ifstream regionData(region.path()); 139 | for (uint16_t chunk = 0; chunk < REGIONSIZE * REGIONSIZE; chunk++) { 140 | regionData.read(buffer, 4); 141 | 142 | if (*((uint32_t *)&buffer) == 0) { 143 | continue; 144 | } 145 | 146 | savedWorld.minX = 147 | std::min(savedWorld.minX, int32_t((regionX << 5) + (chunk & 0x1f))); 148 | savedWorld.maxX = 149 | std::max(savedWorld.maxX, int32_t((regionX << 5) + (chunk & 0x1f))); 150 | savedWorld.minZ = 151 | std::min(savedWorld.minZ, int32_t((regionZ << 5) + (chunk >> 5))); 152 | savedWorld.maxZ = 153 | std::max(savedWorld.maxZ, int32_t((regionZ << 5) + (chunk >> 5))); 154 | } 155 | } 156 | 157 | // Convert region indexes to blocks 158 | savedWorld.minX = savedWorld.minX << 4; 159 | savedWorld.minZ = savedWorld.minZ << 4; 160 | savedWorld.maxX = ((savedWorld.maxX + 1) << 4) - 1; 161 | savedWorld.maxZ = ((savedWorld.maxZ + 1) << 4) - 1; 162 | 163 | savedWorld.minY = mcmap::constants::min_y; 164 | savedWorld.maxY = mcmap::constants::max_y; 165 | 166 | logger::debug("World spans from {}", savedWorld.to_string()); 167 | 168 | return savedWorld; 169 | } 170 | -------------------------------------------------------------------------------- /src/savefile.h: -------------------------------------------------------------------------------- 1 | #include "./helper.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using nlohmann::json; 11 | namespace fs = std::filesystem; 12 | 13 | struct Dimension { 14 | std::string ns, id; 15 | 16 | Dimension(std::string ns, std::string id) : ns(ns), id(id){}; 17 | Dimension(std::string _id); 18 | 19 | bool operator==(const Dimension &) const; 20 | 21 | fs::path suffix() const; 22 | 23 | std::string to_string() { return fmt::format("{}:{}", ns, id); }; 24 | }; 25 | 26 | struct SaveFile { 27 | std::string name; 28 | std::time_t last_played; 29 | fs::path folder; 30 | std::vector dimensions; 31 | 32 | SaveFile(); 33 | SaveFile(const fs::path &_folder); 34 | 35 | bool valid() const { return last_played; } 36 | void getDimensions(); 37 | 38 | fs::path region(const Dimension &) const; 39 | World::Coordinates getWorld(const Dimension &); 40 | }; 41 | 42 | bool assert_save(const fs::path &root); 43 | 44 | void to_json(json &, const Dimension &); 45 | void to_json(json &, const SaveFile &); 46 | -------------------------------------------------------------------------------- /src/section.cpp: -------------------------------------------------------------------------------- 1 | #include "./section.h" 2 | #include "./chunk_format_versions/section_format.hpp" 3 | #include 4 | #include 5 | 6 | namespace mcmap { 7 | namespace versions { 8 | std::map> init = { 9 | {3100, init_versions::v3100}, {2840, init_versions::v2840}, 10 | {2534, init_versions::v2534}, {1628, init_versions::v1628}, 11 | {0, init_versions::catchall}, 12 | }; 13 | } // namespace versions 14 | } // namespace mcmap 15 | 16 | const Colors::Block _void; 17 | 18 | Section::Section() : colors{&_void} { 19 | // The `colors` array needs to contain at least a color to have a defined 20 | // behavious when uninitialized. `color_at` is called 4096x per section, it is 21 | // critical for it to avoid if-elses. 22 | 23 | // This is set to the maximum index available as not to trigger a beacon 24 | // detection by error 25 | beaconIndex = std::numeric_limits::max(); 26 | 27 | // Make sure all the blocks are air - thanks to the default value in `colors` 28 | blocks.fill(std::numeric_limits::min()); 29 | lights.fill(std::numeric_limits::min()); 30 | } 31 | 32 | void Section::loadPalette(const Colors::Palette &defined) { 33 | // Pick the colors from the Palette 34 | for (const auto &color : palette) { 35 | const string namespacedId = color["Name"].get(); 36 | auto query = defined.find(namespacedId); 37 | 38 | if (query == defined.end()) { 39 | logger::error("Color of block {} not found", namespacedId); 40 | colors.push_back(&_void); 41 | } else { 42 | colors.push_back(&query->second); 43 | if (namespacedId == "minecraft:beacon") 44 | beaconIndex = colors.size() - 1; 45 | } 46 | } 47 | } 48 | 49 | Section::Section(const nbt::NBT &raw_section, const int dataVersion, 50 | const Coordinates chunk) 51 | : Section() { 52 | 53 | // Get data from the NBT 54 | Y = raw_section["Y"].get(); 55 | this->parent_chunk_coordinates = chunk; 56 | 57 | auto init_it = compatible(mcmap::versions::init, dataVersion); 58 | 59 | if (init_it != mcmap::versions::init.end()) { 60 | init_it->second(this, raw_section); 61 | } 62 | 63 | // Iron out potential corruption errors 64 | for (block_array::reference index : blocks) { 65 | if (index > palette.size() - 1) { 66 | logger::trace("Malformed section: block is undefined in palette"); 67 | index = 0; 68 | } 69 | } 70 | 71 | // Import lighting data if present 72 | if (raw_section.contains("BlockLight")) { 73 | const nbt::NBT::tag_byte_array_t *blockLights = 74 | raw_section["BlockLight"].get(); 75 | 76 | if (blockLights->size() == 2048) { 77 | for (nbt::NBT::tag_byte_array_t::size_type i = 0; i < blockLights->size(); 78 | i++) { 79 | lights[i] = blockLights->at(i); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/section.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./colors.h" 4 | #include <2DCoordinates.hpp> 5 | #include 6 | 7 | struct Section { 8 | // Each block is encoded on 16 bits as an index in the palette - where only 12 9 | // bits are necessary 10 | using block_array = std::array; 11 | 12 | // Each index in the array above points to a block in the palette and a color 13 | // in the following array 14 | using color_array = std::vector; 15 | using light_array = std::array; 16 | 17 | // The vertical index of the section 18 | int8_t Y; 19 | // Coordinates of the parent chunk, used only for debug 20 | Coordinates parent_chunk_coordinates; 21 | 22 | block_array blocks; 23 | color_array colors; 24 | light_array lights; 25 | std::vector palette; 26 | 27 | block_array::value_type beaconIndex; 28 | 29 | Section(); 30 | Section(const nbt::NBT &, const int, const Coordinates = {0, 0}); 31 | Section(Section &&other) { *this = std::move(other); } 32 | 33 | Section &operator=(Section &&other) { 34 | Y = other.Y; 35 | parent_chunk_coordinates = other.parent_chunk_coordinates; 36 | 37 | beaconIndex = other.beaconIndex; 38 | 39 | blocks = std::move(other.blocks); 40 | lights = std::move(other.lights); 41 | colors = std::move(other.colors); 42 | palette = std::move(other.palette); 43 | return *this; 44 | } 45 | 46 | inline bool empty() const { 47 | // Check the state of the section by looking at its palette: a section with 48 | // only air does not need to be rendered 49 | return palette.empty() || 50 | (palette.size() == 1 && 51 | palette[0]["Name"].get() == "minecraft:air"); 52 | } 53 | 54 | inline block_array::value_type block_at(uint8_t x, uint8_t y, 55 | uint8_t z) const { 56 | return blocks[x + 16 * z + 16 * 16 * y]; 57 | } 58 | 59 | inline color_array::value_type color_at(uint8_t x, uint8_t y, 60 | uint8_t z) const { 61 | return colors[blocks[x + 16 * z + 16 * 16 * y]]; 62 | } 63 | 64 | inline light_array::value_type light_at(uint8_t x, uint8_t y, 65 | uint8_t z) const { 66 | const uint16_t index = x + 16 * z + 16 * 16 * y; 67 | 68 | return (index % 2 ? lights[index / 2] >> 4 : lights[index / 2] & 0x0f); 69 | } 70 | 71 | inline const nbt::NBT &state_at(uint8_t x, uint8_t y, uint8_t z) const { 72 | return palette[blocks[x + 16 * z + 16 * 16 * y]]; 73 | } 74 | 75 | void loadPalette(const Colors::Palette &); 76 | }; 77 | -------------------------------------------------------------------------------- /src/settings.cpp: -------------------------------------------------------------------------------- 1 | #include "./settings.h" 2 | #include "logger.hpp" 3 | 4 | void Settings::to_json(json &j, const Settings::WorldOptions &o) { 5 | j["mode"] = o.mode; 6 | j["output"] = o.outFile.string(); 7 | j["colors"] = o.colorFile.string(); 8 | 9 | j["save"] = o.save; 10 | j["dimension"] = o.dim; 11 | 12 | j["coordinates"] = o.boundaries.to_string(); 13 | j["padding"] = o.padding; 14 | 15 | j["hideWater"] = o.hideWater; 16 | j["hideBeacons"] = o.hideBeacons; 17 | j["shading"] = o.shading; 18 | j["lighting"] = o.lighting; 19 | 20 | j["memory"] = o.mem_limit; 21 | j["tile"] = o.tile_size; 22 | } 23 | -------------------------------------------------------------------------------- /src/settings.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONS_H_ 2 | #define OPTIONS_H_ 3 | 4 | #include "./colors.h" 5 | #include "./helper.h" 6 | #include "./savefile.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define UNDEFINED 0x7FFFFFFF 13 | 14 | namespace Settings { 15 | const string OUTPUT_DEFAULT = "output.png"; 16 | const string OUTPUT_TILED_DEFAULT = "output"; 17 | const size_t PADDING_DEFAULT = 5; 18 | const size_t TILE_SIZE_DEFAULT = 0; 19 | 20 | enum Action { RENDER, DUMPCOLORS, HELP }; 21 | 22 | struct WorldOptions { 23 | // Execution mode 24 | Action mode; 25 | 26 | // Files to use 27 | fs::path outFile, colorFile; 28 | 29 | // Map boundaries 30 | SaveFile save; 31 | Dimension dim; 32 | World::Coordinates boundaries; 33 | 34 | // Image settings 35 | uint16_t padding; 36 | bool hideWater, hideBeacons, shading, lighting; 37 | size_t tile_size; // 0 means no tiling 38 | 39 | // Marker storage 40 | uint8_t totalMarkers; 41 | std::array markers; 42 | 43 | // Memory limits 44 | size_t mem_limit; 45 | size_t fragment_size; 46 | 47 | WorldOptions() 48 | : mode(RENDER), outFile(OUTPUT_DEFAULT), colorFile(""), save(), 49 | dim("overworld") { 50 | 51 | boundaries.setUndefined(); 52 | boundaries.minY = mcmap::constants::min_y; 53 | boundaries.maxY = mcmap::constants::max_y; 54 | 55 | hideWater = hideBeacons = shading = lighting = false; 56 | padding = PADDING_DEFAULT; 57 | tile_size = TILE_SIZE_DEFAULT; 58 | 59 | totalMarkers = 0; 60 | 61 | // Default 3.5G of memory maximum 62 | mem_limit = 3500 * uint64_t(1024 * 1024); 63 | // Render whole regions at once 64 | fragment_size = 1024; 65 | } 66 | 67 | fs::path regionDir() const { return save.region(dim); } 68 | }; 69 | 70 | void to_json(json &j, const WorldOptions &o); 71 | 72 | } // namespace Settings 73 | 74 | #endif // OPTIONS_H_ 75 | -------------------------------------------------------------------------------- /src/worldloader.cpp: -------------------------------------------------------------------------------- 1 | #include "./worldloader.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace Terrain { 7 | 8 | Data::Chunk empty_chunk; 9 | 10 | bool decompressChunk(const uint32_t offset, FILE *regionHandle, 11 | uint8_t *chunkBuffer, uint64_t *length, 12 | const std::filesystem::path &filename) { 13 | uint8_t zData[COMPRESSED_BUFFER]; 14 | 15 | if (0 != FSEEK(regionHandle, offset, SEEK_SET)) { 16 | logger::debug("Accessing chunk data in file {} failed: {}", 17 | filename.string(), strerror(errno)); 18 | return false; 19 | } 20 | 21 | // Read the 5 bytes that give the size and type of data 22 | if (5 != fread(zData, sizeof(uint8_t), 5, regionHandle)) { 23 | logger::debug("Reading chunk size from region file {} failed: {}", 24 | filename.string(), strerror(errno)); 25 | return false; 26 | } 27 | 28 | // Read the size on the first 4 bytes, discard the type 29 | *length = translate(zData); 30 | (*length)--; // Sometimes the data is 1 byte smaller 31 | 32 | if (fread(zData, sizeof(uint8_t), *length, regionHandle) != *length) { 33 | logger::debug("Not enough data for chunk: {}", strerror(errno)); 34 | return false; 35 | } 36 | 37 | z_stream zlibStream; 38 | memset(&zlibStream, 0, sizeof(z_stream)); 39 | zlibStream.next_in = (Bytef *)zData; 40 | zlibStream.next_out = (Bytef *)chunkBuffer; 41 | zlibStream.avail_in = *length; 42 | zlibStream.avail_out = DECOMPRESSED_BUFFER; 43 | inflateInit2(&zlibStream, 32 + MAX_WBITS); 44 | 45 | int status = inflate(&zlibStream, Z_FINISH); // decompress in one step 46 | inflateEnd(&zlibStream); 47 | 48 | if (status != Z_STREAM_END) { 49 | logger::debug("Decompressing chunk data failed: {}", zError(status)); 50 | return false; 51 | } 52 | 53 | *length = zlibStream.total_out; 54 | return true; 55 | } 56 | 57 | void Data::loadChunk(const ChunkCoordinates coords) { 58 | FILE *regionHandle; 59 | uint8_t regionHeader[REGION_HEADER_SIZE], chunkBuffer[DECOMPRESSED_BUFFER]; 60 | int32_t regionX = REGION(coords.x), regionZ = REGION(coords.z), 61 | cX = coords.x & 0x1f, cZ = coords.z & 0x1f; 62 | uint64_t length; 63 | 64 | std::filesystem::path regionFile = std::filesystem::path(regionDir) /= 65 | fmt::format("r.{}.{}.mca", regionX, regionZ); 66 | 67 | if (!std::filesystem::exists(regionFile)) { 68 | logger::trace("Region file r.{}.{}.mca does not exist, skipping ..", 69 | regionX, regionZ); 70 | return; 71 | } 72 | 73 | if (!(regionHandle = fopen(regionFile.string().c_str(), "rb"))) { 74 | logger::error("Opening region file `{}` failed: {}", regionFile.string(), 75 | strerror(errno)); 76 | return; 77 | } 78 | 79 | // Then, we read the header (of size 4K) storing the chunks locations 80 | if (fread(regionHeader, sizeof(uint8_t), REGION_HEADER_SIZE, regionHandle) != 81 | REGION_HEADER_SIZE) { 82 | logger::error("Region header too short in `{}`", regionFile.string()); 83 | fclose(regionHandle); 84 | return; 85 | } 86 | 87 | const uint32_t offset = 88 | (translate(regionHeader + ((cZ << 5) + cX) * 4) >> 8) * 4096; 89 | 90 | if (!offset || !decompressChunk(offset, regionHandle, chunkBuffer, &length, 91 | regionFile)) { 92 | fclose(regionHandle); 93 | return; 94 | } 95 | 96 | nbt::NBT data; 97 | 98 | if (!nbt::parse(chunkBuffer, length, data) || !Chunk::assert_chunk(data)) { 99 | fclose(regionHandle); 100 | logger::trace("Chunk parsing failed for chunk {} {} in {}", coords.x, 101 | coords.z, regionFile.string()); 102 | return; 103 | } 104 | 105 | fclose(regionHandle); 106 | 107 | chunks[coords] = Chunk(data, palette, coords); 108 | } 109 | 110 | const Data::Chunk &Data::chunkAt(const ChunkCoordinates coords, 111 | const Map::Orientation o, bool surround) { 112 | if (chunks.find(coords) == chunks.end()) 113 | loadChunk(coords); 114 | 115 | if (surround) { 116 | ChunkCoordinates left = coords + left_in(o); 117 | ChunkCoordinates right = coords + right_in(o); 118 | 119 | if (chunks.find(left) == chunks.end()) 120 | loadChunk(left); 121 | if (chunks.find(right) == chunks.end()) 122 | loadChunk(right); 123 | } 124 | 125 | auto query = chunks.find(coords); 126 | if (query == chunks.end()) { 127 | logger::trace("Chunk loading failed for {} {}", coords.x, coords.z); 128 | return empty_chunk; 129 | } 130 | 131 | return query->second; 132 | } 133 | 134 | void Data::free_chunk(const ChunkCoordinates coords) { 135 | auto query = chunks.find(coords); 136 | 137 | if (query != chunks.end()) 138 | chunks.erase(query); 139 | } 140 | 141 | } // namespace Terrain 142 | -------------------------------------------------------------------------------- /src/worldloader.h: -------------------------------------------------------------------------------- 1 | #ifndef WORLDLOADER_H_ 2 | #define WORLDLOADER_H_ 3 | 4 | #include "./chunk.h" 5 | #include "./helper.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace Terrain { 11 | 12 | struct Data { 13 | using Chunk = mcmap::Chunk; 14 | using ChunkCoordinates = mcmap::Chunk::coordinates; 15 | using ChunkStore = std::map; 16 | 17 | // The coordinates of the loaded chunks. This coordinates maps 18 | // the CHUNKS loaded, not the blocks 19 | World::Coordinates map; 20 | 21 | // The loaded chunks, organized as a map of coordinatesxchunk 22 | ChunkStore chunks; 23 | 24 | fs::path regionDir; 25 | const Colors::Palette &palette; 26 | 27 | // Default constructor 28 | explicit Data(const World::Coordinates &coords, 29 | const std::filesystem::path &dir, const Colors::Palette &p) 30 | : regionDir(dir), palette(p) { 31 | map.minX = CHUNK(coords.minX); 32 | map.minZ = CHUNK(coords.minZ); 33 | map.maxX = CHUNK(coords.maxX); 34 | map.maxZ = CHUNK(coords.maxZ); 35 | } 36 | 37 | // Chunk pre-processing methods 38 | void stripChunk(std::vector *); 39 | void inflateChunk(std::vector *); 40 | 41 | // Chunk loading - should never be used, called by chunkAt in case of chunk 42 | // fault 43 | void loadChunk(const ChunkCoordinates); 44 | 45 | // Access a chunk from the save file 46 | const Chunk &chunkAt(const ChunkCoordinates, const Map::Orientation, bool); 47 | 48 | // Mark a chunk as done and ready for deletion 49 | void free_chunk(const ChunkCoordinates); 50 | }; 51 | 52 | } // namespace Terrain 53 | 54 | #endif // WORLDLOADER_H_ 55 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB TESTS *cpp) 2 | 3 | IF(GTest_FOUND) 4 | ADD_EXECUTABLE(run_tests ${TESTS}) 5 | 6 | TARGET_LINK_LIBRARIES(run_tests 7 | PRIVATE 8 | GTest::gtest 9 | GTest::gtest_main 10 | fmt::fmt-header-only 11 | ZLIB::ZLIB 12 | mcmap_core) 13 | ENDIF() 14 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Requires `gtest` to run. 4 | 5 | Compile using `-DTESTS_=1` when calling `cmake`. 6 | -------------------------------------------------------------------------------- /tests/samples.h: -------------------------------------------------------------------------------- 1 | extern unsigned char chunk_nbt[]; 2 | extern unsigned char level_nbt[]; 3 | extern unsigned int chunk_nbt_len; 4 | extern unsigned int level_nbt_len; 5 | -------------------------------------------------------------------------------- /tests/test_canvas.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/canvas.h" 2 | #include 3 | 4 | TEST(TestCanvas, TestCreateDefault) { 5 | Canvas c1; 6 | 7 | ASSERT_TRUE(c1.type == Canvas::EMPTY); 8 | ASSERT_TRUE(c1.width() == c1.height() && c1.width() == 0); 9 | 10 | c1 = Canvas(Canvas::BYTES); 11 | 12 | ASSERT_TRUE(c1.type == Canvas::BYTES); 13 | ASSERT_TRUE(c1.width() == c1.height() && c1.width() == 0); 14 | 15 | c1 = Canvas(Canvas::CANVAS); 16 | 17 | ASSERT_TRUE(c1.type == Canvas::CANVAS); 18 | ASSERT_TRUE(c1.width() == c1.height() && c1.width() == 0); 19 | } 20 | 21 | TEST(TestCanvas, TestGetLine) { 22 | uint8_t buffer[1000]; 23 | Canvas c1; 24 | 25 | ASSERT_FALSE(c1.getLine(&buffer[0], 1000, 0)); 26 | 27 | c1 = Canvas(Canvas::BYTES); 28 | ASSERT_FALSE(c1.getLine(&buffer[0], 1000, 0)); 29 | 30 | c1 = Canvas(Canvas::CANVAS); 31 | ASSERT_FALSE(c1.getLine(&buffer[0], 1000, 0)); 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_chunk.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/chunk.h" 2 | #include "../src/include/nbt/parser.hpp" 3 | #include "./samples.h" 4 | #include 5 | 6 | TEST(TestChunkStatic, TestAssert) { 7 | nbt::NBT chunk; 8 | ASSERT_FALSE(mcmap::Chunk::assert_chunk(chunk)); 9 | nbt::parse(chunk_nbt, chunk_nbt_len, chunk); 10 | ASSERT_TRUE(mcmap::Chunk::assert_chunk(chunk)); 11 | } 12 | 13 | class TestChunkCoordinates : public ::testing::Test { 14 | protected: 15 | mcmap::Chunk::coordinates pos; 16 | TestChunkCoordinates() : pos({5, -8}) {} 17 | }; 18 | 19 | TEST_F(TestChunkCoordinates, TestCreate) { 20 | ASSERT_EQ(pos.x, 5); 21 | ASSERT_EQ(pos.z, -8); 22 | } 23 | 24 | TEST_F(TestChunkCoordinates, TestAdd) { 25 | mcmap::Chunk::coordinates sum = pos + mcmap::Chunk::coordinates({-5, 8}); 26 | 27 | ASSERT_EQ(sum.x, 0); 28 | ASSERT_EQ(sum.z, 0); 29 | } 30 | 31 | TEST_F(TestChunkCoordinates, TestEqual) { 32 | mcmap::Chunk::coordinates other = {0, 0}; 33 | other.x = 5; 34 | other.z = -8; 35 | 36 | ASSERT_EQ(pos, other); 37 | } 38 | 39 | TEST_F(TestChunkCoordinates, TestOrder) { 40 | for (int i = -128; i < 129; i++) { 41 | for (int j = -128; j < 129; j++) { 42 | mcmap::Chunk::coordinates x = {i, j}, y = {j, i}; 43 | // Ensure only one of x 3 | 4 | Colors::Color water = "#0743c832"; 5 | Colors::Color dummy = "#ffffff"; 6 | 7 | /* BSON representation of: 8 | { 9 | "field1": 5, 10 | "field2": { 11 | "field3": { 12 | "field4": [0] 13 | } 14 | } 15 | } 16 | */ 17 | const std::vector bad_palette = { 18 | 0x3f, 0x0, 0x0, 0x0, 0x10, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x0, 0x5, 19 | 0x0, 0x0, 0x0, 0x3, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x0, 0x26, 0x0, 20 | 0x0, 0x0, 0x3, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x0, 0x19, 0x0, 0x0, 21 | 0x0, 0x4, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x34, 0x0, 0xc, 0x0, 0x0, 0x0, 22 | 0x10, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; 23 | 24 | /* BSON representation of: 25 | { 26 | "minecraft:water": [0, 0, 0, 0] 27 | } 28 | */ 29 | const std::vector nowater_palette = { 30 | 0x37, 0x0, 0x0, 0x0, 0x4, 0x6d, 0x69, 0x6e, 0x65, 0x63, 0x72, 31 | 0x61, 0x66, 0x74, 0x3a, 0x77, 0x61, 0x74, 0x65, 0x72, 0x0, 0x21, 32 | 0x0, 0x0, 0x0, 0x10, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 33 | 0x31, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x32, 0x0, 0x0, 0x0, 34 | 0x0, 0x0, 0x10, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; 35 | 36 | TEST(TestColor, TestCreate) { 37 | Colors::Color c; 38 | 39 | ASSERT_TRUE(c.empty()); 40 | 41 | ASSERT_FALSE(water.empty()); 42 | ASSERT_EQ(water.R, 7); 43 | ASSERT_EQ(water.G, 67); 44 | ASSERT_EQ(water.B, 200); 45 | ASSERT_EQ(water.ALPHA, 50); 46 | } 47 | 48 | TEST(TestColor, TestEmpty) { 49 | Colors::Color c; 50 | c.R = c.G = c.B = c.ALPHA; 51 | ASSERT_TRUE(c.empty()); 52 | 53 | Colors::Color cR = c, cG = c, cB = c; 54 | cR.R = 1; 55 | cG.G = 1; 56 | cB.B = 1; 57 | 58 | ASSERT_FALSE(cR.empty()); 59 | ASSERT_FALSE(cG.empty()); 60 | ASSERT_FALSE(cB.empty()); 61 | } 62 | 63 | TEST(TestColor, TestOpacity) { 64 | Colors::Color c; 65 | ASSERT_TRUE(c.transparent()); 66 | ASSERT_FALSE(c.opaque()); 67 | 68 | c.ALPHA = 1; 69 | ASSERT_FALSE(c.transparent()); 70 | ASSERT_FALSE(c.opaque()); 71 | 72 | c.ALPHA = 255; 73 | ASSERT_FALSE(c.transparent()); 74 | ASSERT_TRUE(c.opaque()); 75 | } 76 | 77 | TEST(TestColor, TestJson) { 78 | Colors::Color b = water, translated; 79 | translated = nlohmann::json(b).get(); 80 | 81 | ASSERT_EQ(b, translated); 82 | } 83 | 84 | TEST(TestBlock, TestCreateDefault) { 85 | Colors::Block b; 86 | 87 | ASSERT_TRUE(b.type == Colors::FULL); 88 | ASSERT_TRUE(b.primary.empty()); 89 | ASSERT_TRUE(b.secondary.empty()); 90 | } 91 | 92 | TEST(TestBlock, TestCreateType) { 93 | Colors::Block b = Colors::Block(Colors::drawSlab, dummy); 94 | 95 | ASSERT_TRUE(b.type == Colors::drawSlab); 96 | ASSERT_FALSE(b.primary.empty()); 97 | ASSERT_TRUE(b.secondary.empty()); 98 | } 99 | 100 | TEST(TestBlock, TestCreateTypeAccent) { 101 | Colors::Block b = Colors::Block(Colors::drawStair, dummy, dummy); 102 | 103 | ASSERT_TRUE(b.type == Colors::drawStair); 104 | ASSERT_FALSE(b.primary.empty()); 105 | ASSERT_FALSE(b.secondary.empty()); 106 | } 107 | 108 | TEST(TestBlock, TestEqualOperator) { 109 | Colors::Block b1 = Colors::Block(Colors::drawBeam, dummy), b2 = b1; 110 | 111 | ASSERT_EQ(b1, b2); 112 | b1.type = Colors::drawTransparent; 113 | ASSERT_NE(b1, b2); 114 | 115 | b2 = Colors::Block(Colors::drawTransparent, dummy, dummy); 116 | ASSERT_NE(b1, b2); 117 | } 118 | 119 | TEST(TestBlock, TestJson) { 120 | Colors::Block b, translated; 121 | b = Colors::Block(Colors::drawStair, dummy, dummy); 122 | translated = nlohmann::json(b).get(); 123 | 124 | ASSERT_EQ(b, translated); 125 | } 126 | 127 | TEST(TestPalette, TestJson) { 128 | Colors::Palette p, translated; 129 | p.insert(std::pair("minecraft:water", Colors::Block(Colors::FULL, water))); 130 | translated = nlohmann::json(p).get(); 131 | 132 | ASSERT_EQ(p, translated); 133 | } 134 | 135 | TEST(TestColorImport, TestLoadEmbedded) { 136 | Colors::Palette p; 137 | 138 | ASSERT_TRUE(Colors::load(&p)); 139 | ASSERT_TRUE(p.size()); 140 | } 141 | 142 | TEST(TestColorImport, TestLoadFile) { 143 | Colors::Palette p; 144 | 145 | ASSERT_TRUE(Colors::load(&p, json::from_bson(nowater_palette))); 146 | ASSERT_TRUE(p.size()); 147 | ASSERT_TRUE(p.find("minecraft:water") != p.end()); 148 | ASSERT_TRUE(p["minecraft:water"].primary.transparent()); 149 | } 150 | 151 | TEST(TestColorImport, TestLoadNoFile) { 152 | Colors::Palette p; 153 | 154 | ASSERT_FALSE(Colors::load(&p, fs::path("/non-existent"))); 155 | ASSERT_FALSE(p.size()); 156 | } 157 | 158 | TEST(TestColorImport, TestLoadBadFormat) { 159 | Colors::Palette p; 160 | 161 | ASSERT_FALSE(Colors::load(&p, json::from_bson(bad_palette))); 162 | ASSERT_FALSE(p.size()); 163 | } 164 | -------------------------------------------------------------------------------- /tests/test_compat.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST(TestCompat, TestReturn) { 6 | std::map versions = { 7 | {0, 0}, {1, 0}, {2, 0}, {4, 0}, {8, 0}, {16, 0}, 8 | }; 9 | 10 | std::map::const_iterator it; 11 | 12 | it = compatible(versions, 0); 13 | ASSERT_NE(it, versions.end()); 14 | ASSERT_EQ(it->first, 0); 15 | 16 | for (int i = 1; i < 20; i++) { 17 | it = compatible(versions, i); 18 | ASSERT_NE(it, versions.end()); 19 | ASSERT_EQ(it->first, 1 << int(std::log2(double(i)))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/test_coordinates.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST(TestCoordinates, TestCreateDefault) { 5 | Map::Coordinates minimap; 6 | ASSERT_TRUE(minimap.isUndefined()); 7 | } 8 | 9 | TEST(TestCoordinates, TestCreateCopy) { 10 | Map::Coordinates original(-100, 0, -100, 100, 255, 100, Map::SE), 11 | copy; 12 | 13 | copy = original; 14 | 15 | ASSERT_EQ(original.minX, copy.minX); 16 | ASSERT_EQ(original.minZ, copy.minZ); 17 | ASSERT_EQ(original.maxX, copy.maxX); 18 | ASSERT_EQ(original.maxZ, copy.maxZ); 19 | ASSERT_EQ(original.orientation, copy.orientation); 20 | } 21 | 22 | TEST(TestCoordinates, TestCrop) { 23 | Map::Coordinates original(-1000, 0, -1000, 100, 255, 100), 24 | copy(-100, 0, -100, 1000, 255, 1000), 25 | intended(-100, 0, -100, 100, 255, 100); 26 | 27 | original.crop(copy); 28 | ASSERT_EQ(original, intended); 29 | } 30 | 31 | TEST(TestCoordinates, TestAdd) { 32 | Map::Coordinates original(-1000, 0, -1000, 100, 255, 100), 33 | copy(-100, 0, -100, 1000, 255, 1000), 34 | intended(-1000, 0, -1000, 1000, 255, 1000); 35 | 36 | original += copy; 37 | ASSERT_EQ(original, intended); 38 | } 39 | -------------------------------------------------------------------------------- /tests/test_nbt.cpp: -------------------------------------------------------------------------------- 1 | #include "./samples.h" 2 | #include 3 | #include 4 | 5 | #define BUFFERSIZE 2000000 6 | 7 | TEST(TestNBT, TestCreateName) { 8 | nbt::NBT test; 9 | test.set_name("test NBT"); 10 | ASSERT_EQ(test.get_name(), "test NBT"); 11 | } 12 | 13 | TEST(TestNBT, TestCreateEnd) { 14 | nbt::NBT test; 15 | ASSERT_TRUE(test.empty()); 16 | } 17 | 18 | TEST(TestNBT, TestCreateByte) { 19 | nbt::NBT test(std::numeric_limits::max()); 20 | 21 | ASSERT_FALSE(test.empty()); 22 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_byte); 23 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 24 | } 25 | 26 | TEST(TestNBT, TestCreateShort) { 27 | nbt::NBT test(std::numeric_limits::max()); 28 | 29 | ASSERT_FALSE(test.empty()); 30 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_short); 31 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 32 | } 33 | 34 | TEST(TestNBT, TestCreateInt) { 35 | nbt::NBT test(std::numeric_limits::max()); 36 | 37 | ASSERT_FALSE(test.empty()); 38 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_int); 39 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 40 | } 41 | 42 | TEST(TestNBT, TestCreateLong) { 43 | nbt::NBT test(std::numeric_limits::max()); 44 | 45 | ASSERT_FALSE(test.empty()); 46 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_long); 47 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 48 | } 49 | 50 | TEST(TestNBT, TestCreateFloat) { 51 | nbt::NBT test(std::numeric_limits::max()); 52 | 53 | ASSERT_FALSE(test.empty()); 54 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_float); 55 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 56 | } 57 | 58 | TEST(TestNBT, TestCreateDouble) { 59 | nbt::NBT test(std::numeric_limits::max()); 60 | 61 | ASSERT_FALSE(test.empty()); 62 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_double); 63 | ASSERT_EQ(test.get(), std::numeric_limits::max()); 64 | } 65 | 66 | TEST(TestNBT, TestCreateString) { 67 | nbt::NBT test("key"); 68 | 69 | ASSERT_FALSE(test.empty()); 70 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_string); 71 | ASSERT_EQ(test.get(), "key"); 72 | } 73 | 74 | TEST(TestNBT, TestCreateByteArray) { 75 | std::vector array = {98, 97, 10, 99}; 76 | std::vector uarray = {98, 97, 10, 99}; 77 | std::vector carray = {98, 97, 10, 99}; 78 | 79 | nbt::NBT test(array); 80 | ASSERT_FALSE(test.empty()); 81 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_byte_array); 82 | 83 | std::vector *data = test.get *>(); 84 | for (size_t i = 0; i < array.size(); i++) 85 | ASSERT_EQ(array.at(i), data->at(i)); 86 | 87 | nbt::NBT utest(uarray); 88 | ASSERT_FALSE(test.empty()); 89 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_byte_array); 90 | 91 | std::vector *udata = utest.get *>(); 92 | for (size_t i = 0; i < uarray.size(); i++) 93 | ASSERT_EQ(uarray.at(i), udata->at(i)); 94 | 95 | nbt::NBT ctest(carray); 96 | ASSERT_FALSE(test.empty()); 97 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_byte_array); 98 | 99 | std::vector *cdata = ctest.get *>(); 100 | for (size_t i = 0; i < carray.size(); i++) 101 | ASSERT_EQ(carray.at(i), cdata->at(i)); 102 | } 103 | 104 | TEST(TestNBT, TestCreateList) { 105 | std::vector elements = {"This", "is", "a", "test"}; 106 | std::vector list; 107 | 108 | for (auto &value : elements) 109 | list.push_back(nbt::NBT(value)); 110 | 111 | nbt::NBT test = nbt::NBT(list); 112 | 113 | ASSERT_FALSE(test.empty()); 114 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_list); 115 | 116 | for (size_t i = 0; i < elements.size(); i++) 117 | ASSERT_EQ(elements[i], test[i].get()); 118 | } 119 | 120 | TEST(TestNBT, TestCreateCompound) { 121 | std::vector elements = {"This", "is", "a", "test"}; 122 | std::map compound; 123 | 124 | for (auto &value : elements) 125 | compound[value] = (nbt::NBT(value)); 126 | 127 | nbt::NBT test = nbt::NBT(compound); 128 | 129 | ASSERT_FALSE(test.empty()); 130 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_compound); 131 | 132 | // Iterators seem to be broken, have to delve into that 133 | } 134 | 135 | TEST(TestNBT, TestCreateIntArray) { 136 | std::vector array = {98, 97, 10, 99}; 137 | 138 | nbt::NBT test(array); 139 | ASSERT_FALSE(test.empty()); 140 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_int_array); 141 | 142 | std::vector *data = test.get *>(); 143 | for (size_t i = 0; i < array.size(); i++) 144 | ASSERT_EQ(array.at(i), data->at(i)); 145 | } 146 | 147 | TEST(TestNBT, TestCreateLongArray) { 148 | std::vector array = {98, 97, 10, 99}; 149 | 150 | nbt::NBT test(array); 151 | ASSERT_FALSE(test.empty()); 152 | ASSERT_EQ(test.get_type(), nbt::tag_type::tag_long_array); 153 | 154 | std::vector *data = test.get *>(); 155 | for (size_t i = 0; i < array.size(); i++) 156 | ASSERT_EQ(array.at(i), data->at(i)); 157 | } 158 | 159 | class TestNBTParse : public ::testing::Test { 160 | protected: 161 | nbt::NBT level; 162 | 163 | TestNBTParse() { nbt::parse(level_nbt, level_nbt_len, level); } 164 | }; 165 | 166 | TEST_F(TestNBTParse, TestParse) { 167 | ASSERT_EQ(level.get_type(), nbt::tag_type::tag_compound); 168 | ASSERT_TRUE(level.size()); 169 | ASSERT_EQ(level["Data"].get_type(), nbt::tag_type::tag_compound); 170 | } 171 | 172 | TEST_F(TestNBTParse, TestAccessByte) { 173 | uint8_t value = level["Data"]["Difficulty"].get(); 174 | 175 | ASSERT_TRUE(value); 176 | } 177 | 178 | TEST_F(TestNBTParse, TestAccessShort) { 179 | uint16_t value = level["Data"]["Difficulty"].get(); 180 | 181 | ASSERT_TRUE(value); 182 | } 183 | 184 | TEST_F(TestNBTParse, TestAccessInt) { 185 | int32_t value = level["Data"]["DataVersion"].get(); 186 | 187 | ASSERT_TRUE(value); 188 | } 189 | 190 | TEST_F(TestNBTParse, TestAccessLong) { 191 | uint64_t value = level["Data"]["DayTime"].get(); 192 | 193 | ASSERT_TRUE(value); 194 | } 195 | 196 | TEST_F(TestNBTParse, TestAccessFloat) { 197 | float value = level["Data"]["BorderSafeZone"].get(); 198 | 199 | ASSERT_TRUE(value); 200 | } 201 | 202 | TEST_F(TestNBTParse, TestAccessDouble) { 203 | double value = level["Data"]["BorderSafeZone"].get(); 204 | 205 | ASSERT_TRUE(value); 206 | } 207 | 208 | TEST_F(TestNBTParse, TestCompoundIterator) { 209 | for (auto &el : level) 210 | ASSERT_EQ(el.get_name(), "Data"); 211 | } 212 | 213 | TEST_F(TestNBTParse, TestListIterator) { 214 | nbt::NBT gateways = level["Data"]["DragonFight"]["Gateways"]; 215 | std::vector angles; 216 | 217 | for (auto &el : gateways) { 218 | ASSERT_EQ(el.get_name(), ""); 219 | angles.push_back(el.get()); 220 | } 221 | 222 | ASSERT_TRUE(angles.size()); 223 | ASSERT_NE(find(angles.begin(), angles.end(), 0), angles.end()); 224 | ASSERT_NE(find(angles.begin(), angles.end(), 1), angles.end()); 225 | } 226 | 227 | TEST(TestNBTIterator, TestEndIterator) { 228 | nbt::NBT end; 229 | 230 | ASSERT_EQ(end.begin(), end.end()); 231 | } 232 | 233 | TEST(TestNBTIterator, TestByteIterator) { 234 | auto value = std::numeric_limits::max(); 235 | nbt::NBT container(value); 236 | 237 | auto it = container.begin(); 238 | 239 | ASSERT_NE(it, container.end()); 240 | ASSERT_EQ(it->get(), value); 241 | ASSERT_EQ(++it, container.end()); 242 | } 243 | 244 | TEST(TestNBTIterator, TestShortIterator) { 245 | auto value = std::numeric_limits::max(); 246 | nbt::NBT container(value); 247 | 248 | auto it = container.begin(); 249 | 250 | ASSERT_NE(it, container.end()); 251 | ASSERT_EQ(it->get(), value); 252 | ASSERT_EQ(++it, container.end()); 253 | } 254 | 255 | TEST(TestNBTIterator, TestIntIterator) { 256 | auto value = std::numeric_limits::max(); 257 | nbt::NBT container(value); 258 | 259 | auto it = container.begin(); 260 | 261 | ASSERT_NE(it, container.end()); 262 | ASSERT_EQ(it->get(), value); 263 | ASSERT_EQ(++it, container.end()); 264 | } 265 | 266 | TEST(TestNBTIterator, TestLongIterator) { 267 | auto value = std::numeric_limits::max(); 268 | nbt::NBT container(value); 269 | 270 | auto it = container.begin(); 271 | 272 | ASSERT_NE(it, container.end()); 273 | ASSERT_EQ(it->get(), value); 274 | ASSERT_EQ(++it, container.end()); 275 | } 276 | 277 | TEST(TestNBTIterator, TestFloatIterator) { 278 | auto value = 1.62786728f; 279 | nbt::NBT container(value); 280 | 281 | auto it = container.begin(); 282 | 283 | ASSERT_NE(it, container.end()); 284 | ASSERT_FLOAT_EQ(it->get(), value); 285 | ASSERT_EQ(++it, container.end()); 286 | } 287 | 288 | TEST(TestNBTIterator, TestDoubleIterator) { 289 | auto value = 1.62786728; 290 | nbt::NBT container(value); 291 | 292 | auto it = container.begin(); 293 | 294 | ASSERT_NE(it, container.end()); 295 | ASSERT_DOUBLE_EQ(it->get(), value); 296 | ASSERT_EQ(++it, container.end()); 297 | } 298 | 299 | TEST(TestNBTIterator, TestStringIterator) { 300 | auto value = "This is a nice string."; 301 | nbt::NBT container(value); 302 | 303 | auto it = container.begin(); 304 | 305 | ASSERT_NE(it, container.end()); 306 | ASSERT_EQ(it->get(), value); 307 | ASSERT_EQ(++it, container.end()); 308 | } 309 | 310 | TEST(TestNBTIterator, TestByteArrayIterator) { 311 | std::vector value = {0, 1, 2, 3, 4}; 312 | nbt::NBT container(value); 313 | 314 | auto it = container.begin(); 315 | 316 | ASSERT_NE(it, container.end()); 317 | std::vector data = *it->get *>(); 318 | for (std::vector::size_type i = 0; i < value.size(); i++) 319 | ASSERT_EQ(data[i], value[i]); 320 | ASSERT_EQ(++it, container.end()); 321 | } 322 | 323 | TEST(TestNBTIterator, TestIntArrayIterator) { 324 | std::vector value = {0, 1, 2, 3, 4}; 325 | nbt::NBT container(value); 326 | 327 | auto it = container.begin(); 328 | 329 | ASSERT_NE(it, container.end()); 330 | std::vector data = *it->get *>(); 331 | for (std::vector::size_type i = 0; i < value.size(); i++) 332 | ASSERT_EQ(data[i], value[i]); 333 | ASSERT_EQ(++it, container.end()); 334 | } 335 | 336 | TEST(TestNBTIterator, TestLongArrayIterator) { 337 | std::vector value = {0, 1, 2, 3, 4}; 338 | nbt::NBT container(value); 339 | 340 | auto it = container.begin(); 341 | 342 | ASSERT_NE(it, container.end()); 343 | std::vector data = *it->get *>(); 344 | for (std::vector::size_type i = 0; i < value.size(); i++) 345 | ASSERT_EQ(data[i], value[i]); 346 | ASSERT_EQ(++it, container.end()); 347 | } 348 | -------------------------------------------------------------------------------- /tests/test_section.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/include/nbt/parser.hpp" 2 | #include "../src/section.h" 3 | #include "./samples.h" 4 | #include 5 | 6 | class TestSection : public ::testing::Test { 7 | protected: 8 | Colors::Palette colors; 9 | nbt::NBT sections; 10 | int dataVersion; 11 | 12 | TestSection() { 13 | nbt::NBT chunk; 14 | nbt::parse(chunk_nbt, chunk_nbt_len, chunk); 15 | 16 | dataVersion = chunk["DataVersion"].get(); 17 | sections = chunk["Level"]["Sections"]; 18 | 19 | Colors::load(&colors); 20 | } 21 | }; 22 | 23 | TEST_F(TestSection, TestCreateEmpty) { 24 | Section s; 25 | 26 | for (uint8_t x = 0; x < 16; x++) 27 | for (uint8_t y = 0; y < 16; y++) 28 | for (uint8_t z = 0; z < 16; z++) 29 | ASSERT_EQ(s.color_at(x, y, z)->primary.ALPHA, 0); 30 | } 31 | 32 | TEST_F(TestSection, TestCreateFromEmpty) { 33 | Section s(sections[0], dataVersion); 34 | s.loadPalette(colors); 35 | 36 | for (uint8_t x = 0; x < 16; x++) 37 | for (uint8_t y = 0; y < 16; y++) 38 | for (uint8_t z = 0; z < 16; z++) 39 | ASSERT_EQ(s.color_at(x, y, z)->primary.ALPHA, 0); 40 | } 41 | 42 | TEST_F(TestSection, TestColorPresence) { 43 | auto query = colors.find("minecraft:bedrock"); 44 | bool presence = false; 45 | 46 | Colors::Block bedrock = query->second; 47 | Section s(sections[1], dataVersion); 48 | s.loadPalette(colors); 49 | 50 | ASSERT_NE(s.colors.size(), 0); 51 | 52 | for (auto &block_color : s.colors) 53 | presence |= (*block_color == bedrock); 54 | 55 | ASSERT_TRUE(presence); 56 | } 57 | 58 | TEST_F(TestSection, TestColor) { 59 | auto query = colors.find("minecraft:bedrock"); 60 | 61 | Colors::Block bedrock = query->second; 62 | Section s(sections[1], dataVersion); 63 | s.loadPalette(colors); 64 | 65 | for (uint8_t x = 0; x < 16; x++) 66 | for (uint8_t z = 0; z < 16; z++) 67 | ASSERT_EQ(*s.color_at(x, 0, z), bedrock); 68 | } 69 | 70 | TEST_F(TestSection, TestMetadata) { 71 | Section s(sections[1], dataVersion); 72 | s.loadPalette(colors); 73 | 74 | const nbt::NBT &state = s.state_at(0, 0, 0); 75 | 76 | ASSERT_TRUE(state.contains("Name")); 77 | ASSERT_EQ(state["Name"].get(), "minecraft:bedrock"); 78 | } 79 | --------------------------------------------------------------------------------