├── .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