├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── build.gradle.kts
├── common
├── build.gradle.kts
└── src
│ └── main
│ ├── java
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ ├── LoaderInfo.java
│ │ ├── extension
│ │ └── IPalettedContainerExtension.java
│ │ └── mixin
│ │ ├── BossBarHudMixin.java
│ │ ├── ClientPlayInteractionManagerMixin.java
│ │ ├── ClientPlayNetworkHandlerMixin.java
│ │ ├── ClientWorldMixin.java
│ │ ├── DebugRendererMixin.java
│ │ ├── GameMenuScreenMixin.java
│ │ ├── IntegratedServerLoaderMixin.java
│ │ └── PalettedContainerMixin.java
│ ├── kotlin
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ ├── Events.kt
│ │ ├── Utils.kt
│ │ ├── WorldTools.kt
│ │ ├── config
│ │ └── WorldToolsConfig.kt
│ │ ├── gui
│ │ ├── BrowseDownloadsScreen.kt
│ │ ├── EnterTextField.kt
│ │ └── ManagerScreen.kt
│ │ ├── manager
│ │ ├── BarManager.kt
│ │ ├── CaptureManager.kt
│ │ ├── MessageManager.kt
│ │ └── StatisticManager.kt
│ │ └── storage
│ │ ├── Cacheable.kt
│ │ ├── CustomRegionBasedStorage.kt
│ │ ├── PathTreeNode.kt
│ │ ├── RegionBased.kt
│ │ ├── StorageFlow.kt
│ │ ├── Storeable.kt
│ │ ├── cache
│ │ ├── DataInjectionHandler.kt
│ │ ├── EntityCacheable.kt
│ │ ├── HotCache.kt
│ │ └── LazyUpdatingDelegate.kt
│ │ └── serializable
│ │ ├── AdvancementsStoreable.kt
│ │ ├── BlockEntityLoadable.kt
│ │ ├── CompressLevelStoreable.kt
│ │ ├── EndFlow.kt
│ │ ├── LevelDataStoreable.kt
│ │ ├── MapDataStoreable.kt
│ │ ├── MetadataStoreable.kt
│ │ ├── PlayerStoreable.kt
│ │ ├── RegionBasedChunk.kt
│ │ ├── RegionBasedEntities.kt
│ │ └── StatisticStoreable.kt
│ └── resources
│ ├── assets
│ └── worldtools
│ │ ├── WorldTools.png
│ │ └── lang
│ │ ├── de_de.json
│ │ ├── en_pt.json
│ │ ├── en_us.json
│ │ ├── fr_ca.json
│ │ ├── fr_fr.json
│ │ ├── nl_be.json
│ │ ├── nl_nl.json
│ │ ├── nn_no.json
│ │ ├── no_no.json
│ │ ├── pt_br.json
│ │ ├── pt_pt.json
│ │ ├── ru_ru.json
│ │ ├── zh_cn.json
│ │ └── zh_tw.json
│ ├── worldtools.accesswidener
│ └── worldtools.mixins.common.json
├── fabric
├── build.gradle.kts
├── gradle.properties
└── src
│ └── main
│ ├── java
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ └── fabric
│ │ └── LoaderInfoImpl.java
│ ├── kotlin
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ └── fabric
│ │ ├── WorldToolsFabric.kt
│ │ └── WorldToolsModMenuIntegration.kt
│ └── resources
│ ├── fabric.mod.json
│ └── worldtools.mixins.fabric.json
├── forge
├── build.gradle.kts
├── gradle.properties
└── src
│ └── main
│ ├── java
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ └── forge
│ │ └── LoaderInfoImpl.java
│ ├── kotlin
│ └── org
│ │ └── waste
│ │ └── of
│ │ └── time
│ │ └── forge
│ │ └── WorldToolsForge.kt
│ └── resources
│ ├── META-INF
│ └── mods.toml
│ └── pack.mcmeta
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: WorldTools 1.21.1 Build
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - "1.21.4"
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Check out repository
14 | uses: actions/checkout@v4
15 |
16 | - name: Gradle Wrapper Verification
17 | uses: gradle/wrapper-validation-action@v3
18 |
19 | - name: Setup JDK
20 | uses: actions/setup-java@v4
21 | with:
22 | java-version: '21'
23 | distribution: 'temurin'
24 |
25 | - name: Elevate wrapper permissions
26 | run: chmod +x ./gradlew
27 |
28 | - name: Build WorldTools
29 | uses: gradle/actions/setup-gradle@v3
30 | with:
31 | arguments: build
32 |
33 | - name: Upload Fabric Artifacts
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: WorldTools-fabric
37 | path: fabric/build/libs/*.jar
38 |
39 | - name: Upload Forge Artifacts
40 | uses: actions/upload-artifact@v4
41 | with:
42 | name: WorldTools-forge
43 | path: forge/build/libs/*.jar
44 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Modrinth and CurseForge Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '[0-9]+.[0-9]+.[0-9]+\+1.21.4'
7 |
8 | env:
9 | MINECRAFT_VERSION: "1.21.4"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out repository
16 | uses: actions/checkout@v4
17 |
18 | - name: Gradle Wrapper Verification
19 | uses: gradle/wrapper-validation-action@v3
20 |
21 | - name: Setup JDK
22 | uses: actions/setup-java@v4
23 | with:
24 | java-version: '21'
25 | distribution: 'temurin'
26 |
27 | - name: Elevate wrapper permissions
28 | run: chmod +x ./gradlew
29 |
30 | - name: Build WorldTools
31 | uses: gradle/actions/setup-gradle@v3
32 | with:
33 | arguments: build
34 |
35 | - name: Upload Fabric Artifact
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: WorldTools-fabric-${{ github.ref_name }}
39 | path: fabric/build/libs/WorldTools-fabric-${{ github.ref_name }}.jar
40 |
41 | - name: Upload Forge Artifact
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: WorldTools-forge-${{ github.ref_name }}
45 | path: forge/build/libs/WorldTools-forge-${{ github.ref_name }}.jar
46 |
47 | - name: Generate Changelog
48 | id: changelog
49 | uses: mikepenz/release-changelog-builder-action@v5
50 | with:
51 | commitMode: true
52 | configurationJson: |
53 | {
54 | "pr_template": "- #{{TITLE}} [#{{AUTHOR}}](#{{URL}})"
55 | }
56 |
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 |
60 | - name: Release Fabric
61 | uses: Kir-Antipov/mc-publish@v3.3
62 | with:
63 | changelog: ${{steps.changelog.outputs.changelog}}
64 | curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
65 | curseforge-id: 909868
66 |
67 | modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
68 | modrinth-id: FlFKBOIX
69 |
70 | files: |
71 | fabric/build/libs/WorldTools-fabric-${{ github.ref_name }}.jar
72 | name: WorldTools ${{ github.ref_name }} (Fabric)
73 | version: ${{ github.ref_name }}
74 | loaders: |
75 | fabric
76 | game-versions: |
77 | ${{ env.MINECRAFT_VERSION }}
78 | dependencies: |
79 | fabric-api(required){modrinth:P7dR8mSH}{curseforge:306612}
80 | fabric-language-kotlin(required){modrinth:Ha28R6CL}{curseforge:351264}
81 | cloth-config(required){modrinth:9s6osm5g}{curseforge:348521}
82 |
83 | - name: Release Forge
84 | uses: Kir-Antipov/mc-publish@v3.3
85 | with:
86 | changelog: ${{steps.changelog.outputs.changelog}}
87 | curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
88 | curseforge-id: 909868
89 |
90 | modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
91 | modrinth-id: FlFKBOIX
92 |
93 | files: |
94 | forge/build/libs/WorldTools-forge-${{ github.ref_name }}.jar
95 | name: WorldTools ${{ github.ref_name }} (Forge)
96 | version: ${{ github.ref_name }}
97 | loaders: |
98 | forge
99 | game-versions: |
100 | ${{ env.MINECRAFT_VERSION }}
101 | dependencies: |
102 | kotlinforforge(required){modrinth:ordsPcFz}{curseforge:351264}
103 | cloth-config(required){modrinth:9s6osm5g}{curseforge:348521}
104 |
105 | # TODO: FIX ACTION PERMS ON REPO SETTINGS
106 | # - name: Release Github
107 | # uses: Kir-Antipov/mc-publish@v3.3
108 | # with:
109 | # changelog: ${{steps.changelog.outputs.changelog}}
110 | # name: WorldTools-${{ github.ref_name }}
111 | # version: ${{ github.ref_name }}
112 | # github-token: ${{ secrets.GITHUB_TOKEN }}
113 | # github-files: |
114 | # fabric/build/libs/WorldTools-fabric-${{ github.ref_name }}.jar
115 | # forge/build/libs/WorldTools-forge-${{ github.ref_name }}.jar
116 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .gradle/
3 |
4 | build/
5 | run/
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # WorldTools: World Downloader (Fabric / Forge)
6 |
7 | [](https://www.curseforge.com/minecraft/mc-mods/worldtools)
8 | [](https://modrinth.com/mod/worldtools)
9 | [](https://www.minecraft.net/)
10 | [](https://www.minecraft.net/)
11 | [](https://www.minecraft.net/)
12 | [](https://www.minecraft.net/)
13 | [](https://www.gnu.org/licenses/gpl-3.0.en.html)
14 |
15 | WorldTools is a powerful Minecraft mod that allows you to capture and save high-detail snapshots of server worlds
16 | locally.
17 | It empowers you to download comprehensive information, including chunks, entities,
18 | chests, players, statistics, advancements, and detailed metadata.
19 | WorldTools ensures that you can retain an accurate and unaltered representation of the server's world for analysis,
20 | sharing, or backup purposes on your local machine.
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |

29 |
30 |
31 | ## Features
32 |
33 | - **World Download (_default keybind:_ `F12`)**:
34 | Initiate a quick download by hitting the `F12` key, which can be altered in the keybind settings.
35 | Alternatively, you can access the GUI (_default keybind:_ `F10`) via the escape menu.
36 | The GUI allows you to tailor the capture process according to your requirements.
37 | WorldTools facilitates the capture of a wide range of crucial elements, ensuring no detail is missed.
38 | - Chunks: Terrain, biomes and structures
39 | - Entities: Inventories and attributes of most entities
40 | - Containers: Contents of all tile entities like chests, shulkers, hoppers, furnaces, brewing stands, droppers,
41 | dispensers etc...
42 | - Players: Player positions and inventories
43 | - Statistics: Full personal player statistics
44 | - Advancements: Player advancements and progress
45 | - Special Objects: Maps, Lecterns and Banners
46 | - Detailed Metadata: Exhaustive capture details like modt, server version, timestamps, and more
47 |
48 | - **Easy Access to Saved Worlds**: Your locally captured world save can be found in the single-player worlds list,
49 | allowing you to load and explore it conveniently.
50 |
51 | - **Advanced Configuration**: WorldTools provides a wide range of settings to customize the capture process to your
52 | needs.
53 | Select elements to capture, modify game rules, alter entity NBT data, and configure the capture process in detail.
54 |
55 | ## Getting Started
56 |
57 | ### Fabric
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 1. **Installation**:
69 | - Install Fabric by following the [Fabric Installation Guide](https://fabricmc.net/wiki/install).
70 | - Download the latest Fabric version of WorldTools from
71 | the [releases page](https://github.com/Avanatiker/WorldTools/releases)
72 | - Place the WorldTools Fabric mod JAR file in the "mods" folder of your Fabric installation.
73 |
74 | 2. **Prerequisites**: Make sure you have the following mods installed:
75 | - [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api)
76 | - [fabric-language-kotlin](https://www.curseforge.com/minecraft/mc-mods/fabric-language-kotlin)
77 | - [Cloth Config API](https://www.curseforge.com/minecraft/mc-mods/cloth-config)
78 | - [Mod Menu](https://modrinth.com/mod/modmenu)
79 |
80 | ### Forge
81 |
82 | 1. **Installation**:
83 | - Install Forge by following the [Forge Download Link](https://files.minecraftforge.net/net/minecraftforge/forge/).
84 | - Download the latest Forge version of WorldTools from
85 | the [releases page](https://github.com/Avanatiker/WorldTools/releases)
86 | - Place the WorldTools Forge mod JAR file in the "mods" folder of your Forge installation.
87 |
88 | 2. **Prerequisites**: Make sure you have the following mods installed:
89 | - [Kotlin For Forge](https://www.curseforge.com/minecraft/mc-mods/kotlin-for-forge)
90 | - [Cloth Config API](https://www.curseforge.com/minecraft/mc-mods/cloth-config)
91 |
92 | ### Usage
93 |
94 | 1. **Download**:
95 | - Enable capture mode: Hit `F12` the GUI (on ESC menu) or `/worldtools capture` to start capturing data.
96 | - Play the game normally while WorldTools downloads the all data. You need to open containers like chests to capture
97 | their contents.
98 | - Save captured data: Hit `F12` the GUI (on ESC menu) or `/worldtools capture` again to stop capturing data and save the world.
99 | 2. **Access Downloaded World**: Your downloaded world can be found in the single-player worlds list.
100 |
101 | ### File Structure
102 |
103 | After capturing data, WorldTools creates the following files in the world directory's folder:
104 |
105 | - `Capture Metadata.md`: Contains detailed information about the capture process itself.
106 |
107 | - `Dimension Tree.txt`: Provides a tree of all dimension folder paths of the server, not just the downloaded ones.
108 |
109 | - `Player Entry List.csv`: Lists all players that were online during the capture including all known metadata.
110 |
111 | ## Supported Languages
112 |
113 | For the best user experience, WorldTools is available in the following languages:
114 |
115 | - German
116 | - English (Pirate)
117 | - English (United States)
118 | - French (Canada)
119 | - French (France)
120 | - Dutch (Belgium)
121 | - Dutch (Netherlands)
122 | - Nynorsk (Norwegian)
123 | - Norwegian (Norway)
124 | - Portuguese (Brazil)
125 | - Portuguese (Portugal)
126 | - Russian
127 |
128 | ## Contributing
129 |
130 | Contributions are welcome!
131 | Please read our [Code of Conduct](https://github.com/Avanatiker/WorldTools/blob/master/CODE_OF_CONDUCT.md)
132 | and [Contributing Guidelines](https://github.com/Avanatiker/WorldTools/blob/master/CONTRIBUTING.md) before submitting a
133 | Pull Request.
134 |
135 | 1. Fork the repository and clone it to your local machine.
136 | `git clone https://github.com/Avanatiker/WorldTools`
137 | 2. Create a new branch for your feature.
138 | `git checkout -b my-new-feature`
139 | 3. Make your changes and commit them to your branch.
140 | `git commit -am 'Add some feature'`
141 | 4. Push your changes to your fork.
142 | `git push origin my-new-feature`
143 | 5. Open a Pull Request in this repository.
144 | 6. Your Pull Request will be reviewed and merged as soon as possible.
145 | 7. Wait for the next release to see your changes in action!
146 |
147 | ## Building
148 |
149 | 1. Once forked and cloned, run `./gradlew build` to build the mod for both mod loaders.
150 | 2. IntelliJ IDEA will generate run configurations for both mod loaders that can be used to run the mod in a test
151 | environment.
152 | 3. The Fabric mod JAR file can be found in `fabric/build/libs` and the Forge mod JAR file in `forge/build/libs`.
153 |
154 | ## ToDo
155 |
156 | ### Fixes
157 | - Fix statistics not updated on stop because the packet answer is not received before the world is saved
158 | - Fix on capture switch config button functionality in capture gui
159 | - Dimension, XP, selected item slot, player game type, is not saved to player nbt in level.dat
160 | - EntityLoadable
161 | - Better rendering
162 |
163 | ### Features
164 | - Capture Mode: Choose between two capture modes: Full and Incremental. The Full mode captures all data from the server, while the Incremental mode only captures data that has changed since the last capture.
165 | - Save server datapack to the downloaded world
166 | - Save more entity data (NBT) like trades etc.
167 | - Live statistics: Data usage, time elapsed, etc.
168 |
169 | ## License
170 |
171 | WorldTools is distributed under
172 | the [GNU General Public License v3.0](https://github.com/Avanatiker/WorldTools/blob/master/LICENSE.md).
173 |
174 | ---
175 |
176 | If you have any questions, concerns, or suggestions,
177 | you can visit our [official Discord server](https://discord.gg/3y3ah5BtjB).
178 |
179 | **Disclaimer:** WorldTools is not affiliated with Mojang Studios. Minecraft is a registered trademark of Mojang Studios.
180 | Use of the WorldTools software is subject to the terms outlined in the license agreement.
181 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2 | import net.fabricmc.loom.task.RemapJarTask
3 |
4 | plugins {
5 | kotlin("jvm") version ("2.1.0")
6 | id("architectury-plugin") version "3.4-SNAPSHOT"
7 | id("dev.architectury.loom") version "1.9-SNAPSHOT" apply false
8 | id("com.github.johnrengelman.shadow") version "8.1.1" apply false
9 | }
10 |
11 | architectury {
12 | minecraft = project.properties["minecraft_version"]!! as String
13 | }
14 |
15 | subprojects {
16 | apply(plugin = "dev.architectury.loom")
17 | dependencies {
18 | "minecraft"("com.mojang:minecraft:${project.properties["minecraft_version"]!!}")
19 | "mappings"("net.fabricmc:yarn:${project.properties["yarn_mappings"]}:v2")
20 | }
21 | if (path != ":common") {
22 | apply(plugin = "com.github.johnrengelman.shadow")
23 |
24 | val shadowCommon by configurations.creating {
25 | isCanBeConsumed = false
26 | isCanBeResolved = true
27 | }
28 | val versionWithMCVersion = "${project.properties["mod_version"]!!}+${project.properties["minecraft_version"]!!}"
29 |
30 | tasks.withType {
31 | options.encoding = "UTF-8"
32 | options.release = 21
33 | }
34 |
35 | tasks {
36 | val shadowJarTask = named("shadowJar", ShadowJar::class)
37 | shadowJarTask {
38 | archiveVersion = versionWithMCVersion
39 | archiveClassifier.set("shadow")
40 | configurations = listOf(shadowCommon)
41 | }
42 |
43 | "remapJar"(RemapJarTask::class) {
44 | dependsOn(shadowJarTask)
45 | inputFile = shadowJarTask.flatMap { it.archiveFile }
46 | archiveVersion = versionWithMCVersion
47 | archiveClassifier = ""
48 | }
49 | jar {
50 | enabled = false
51 | }
52 | }
53 | }
54 | }
55 |
56 | allprojects {
57 | apply(plugin = "java")
58 | apply(plugin = "architectury-plugin")
59 | apply(plugin = "maven-publish")
60 | apply(plugin = "org.jetbrains.kotlin.jvm")
61 | base.archivesName.set(project.properties["archives_base_name"]!! as String)
62 | group = project.properties["maven_group"]!!
63 | version = project.properties["mod_version"]!!
64 |
65 | repositories {
66 | maven("https://api.modrinth.com/maven")
67 | maven("https://jitpack.io")
68 | maven("https://server.bbkr.space/artifactory/libs-release") {
69 | name = "CottonMC"
70 | }
71 | maven("https://maven.shedaniel.me/")
72 | maven("https://maven.terraformersmc.com/releases/")
73 | }
74 |
75 | tasks {
76 | compileKotlin {
77 | kotlinOptions.jvmTarget = "21"
78 | }
79 | }
80 |
81 | tasks.withType(JavaCompile::class.java) {
82 | options.encoding = "UTF-8"
83 | options.release = 21
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | architectury { common("fabric", "forge") }
2 |
3 | loom {
4 | accessWidenerPath.set(File("src/main/resources/worldtools.accesswidener"))
5 | }
6 |
7 | repositories {
8 | maven("https://maven.fabricmc.net/") {
9 | name = "Fabric"
10 | }
11 | maven("https://jitpack.io")
12 | mavenCentral()
13 | mavenLocal()
14 | }
15 |
16 | dependencies {
17 | // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies
18 | // Do NOT use other classes from fabric loader
19 | modImplementation("net.fabricmc:fabric-loader:${project.properties["fabric_loader_version"]!!}")
20 | // Add dependencies on the required Kotlin modules.
21 | modImplementation("net.fabricmc:fabric-language-kotlin:${project.properties["fabric_kotlin_version"]!!}")
22 | implementation(annotationProcessor("io.github.llamalad7:mixinextras-common:${project.properties["mixinextras_version"]}")!!)
23 | modCompileOnly("me.shedaniel.cloth:cloth-config-fabric:${project.properties["cloth_config_version"]}") {
24 | exclude(group = "net.fabricmc.fabric-api", module = "fabric-api")
25 | }
26 | }
27 |
28 | tasks.named("remapJar") {
29 | enabled = false
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/LoaderInfo.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time;
2 |
3 | import dev.architectury.injectables.annotations.ExpectPlatform;
4 | import org.jetbrains.annotations.Contract;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | public class LoaderInfo {
8 | @Contract(pure = true)
9 | @ExpectPlatform
10 | public static @NotNull String getVersion() {
11 | return "DEV";
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/extension/IPalettedContainerExtension.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.extension;
2 |
3 | public interface IPalettedContainerExtension {
4 | void setWTIgnoreLock(boolean ignoreLock);
5 | }
6 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/BossBarHudMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import net.minecraft.client.gui.hud.BossBarHud;
4 | import net.minecraft.client.gui.hud.ClientBossBar;
5 | import org.spongepowered.asm.mixin.Mixin;
6 | import org.spongepowered.asm.mixin.injection.At;
7 | import org.spongepowered.asm.mixin.injection.Redirect;
8 | import org.waste.of.time.manager.BarManager;
9 | import org.waste.of.time.manager.CaptureManager;
10 |
11 | import java.util.*;
12 |
13 | @Mixin(BossBarHud.class)
14 | public class BossBarHudMixin {
15 |
16 | /**
17 | * This mixin is used to add the capture bar and the progress bar to the boss bar overlay and to make sure that
18 | * it is always rendered on top of the other boss bars.
19 | *
20 | * @param bossBars The boss bars that are currently being rendered
21 | * @return A collection of boss bars that includes the capture bar and the progress bar
22 | */
23 |
24 | // todo: remove redirects to avoid mod conflicts
25 | // either replace them with injects or use MixinExtras
26 | @Redirect(method = "render", at = @At(value = "INVOKE", target = "Ljava/util/Map;values()Ljava/util/Collection;"))
27 | public Collection modifyValues(Map bossBars) {
28 | if (!CaptureManager.INSTANCE.getCapturing()) return bossBars.values();
29 | List newBossBars = new ArrayList<>(bossBars.size() + 2);
30 | BarManager.INSTANCE.getCaptureBar().ifPresent(newBossBars::add);
31 | BarManager.INSTANCE.progressBar().ifPresent(newBossBars::add);
32 | newBossBars.addAll(bossBars.values());
33 | return newBossBars;
34 | }
35 |
36 | @Redirect(method = "render", at = @At(value = "INVOKE", target = "Ljava/util/Map;isEmpty()Z"))
37 | public boolean modifyIsEmpty(Map bossBars) {
38 | if (!CaptureManager.INSTANCE.getCapturing()) return bossBars.isEmpty();
39 | return bossBars.isEmpty()
40 | && BarManager.INSTANCE.getCaptureBar().isEmpty()
41 | && BarManager.INSTANCE.progressBar().isEmpty();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/ClientPlayInteractionManagerMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import net.minecraft.client.MinecraftClient;
4 | import net.minecraft.client.network.ClientPlayerEntity;
5 | import net.minecraft.client.network.ClientPlayerInteractionManager;
6 | import net.minecraft.entity.Entity;
7 | import net.minecraft.entity.player.PlayerEntity;
8 | import net.minecraft.util.ActionResult;
9 | import net.minecraft.util.Hand;
10 | import net.minecraft.util.hit.BlockHitResult;
11 | import org.spongepowered.asm.mixin.Final;
12 | import org.spongepowered.asm.mixin.Mixin;
13 | import org.spongepowered.asm.mixin.Shadow;
14 | import org.spongepowered.asm.mixin.injection.At;
15 | import org.spongepowered.asm.mixin.injection.Inject;
16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
17 | import org.waste.of.time.Events;
18 |
19 | @Mixin(ClientPlayerInteractionManager.class)
20 | public class ClientPlayInteractionManagerMixin {
21 |
22 | @Final
23 | @Shadow
24 | private MinecraftClient client;
25 |
26 | @Inject(method = "interactBlock", at = @At("HEAD"))
27 | public void interactBlockHead(final ClientPlayerEntity player, final Hand hand, final BlockHitResult hitResult, final CallbackInfoReturnable cir) {
28 | if (client.world == null) return;
29 | Events.INSTANCE.onInteractBlock(client.world, hitResult);
30 | }
31 |
32 | @Inject(method = "interactEntity", at = @At("HEAD"))
33 | public void interactEntityHead(PlayerEntity player, Entity entity, Hand hand, CallbackInfoReturnable cir) {
34 | if (client.world == null) return;
35 | Events.INSTANCE.onInteractEntity(entity);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/ClientPlayNetworkHandlerMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import net.minecraft.client.network.ClientPlayNetworkHandler;
4 | import net.minecraft.network.packet.s2c.play.StatisticsS2CPacket;
5 | import org.spongepowered.asm.mixin.Mixin;
6 | import org.spongepowered.asm.mixin.injection.At;
7 | import org.spongepowered.asm.mixin.injection.Inject;
8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
9 | import org.waste.of.time.manager.CaptureManager;
10 | import org.waste.of.time.storage.serializable.StatisticStoreable;
11 |
12 | @Mixin(ClientPlayNetworkHandler.class)
13 | public class ClientPlayNetworkHandlerMixin {
14 | @Inject(method = "onStatistics", at = @At("RETURN"))
15 | private void onStatistics(StatisticsS2CPacket packet, CallbackInfo ci) {
16 | if (!CaptureManager.INSTANCE.getCapturing()) return;
17 | new StatisticStoreable().emit();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/ClientWorldMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import com.llamalad7.mixinextras.sugar.Local;
4 | import net.minecraft.client.world.ClientWorld;
5 | import net.minecraft.component.type.MapIdComponent;
6 | import net.minecraft.entity.Entity;
7 | import net.minecraft.item.map.MapState;
8 | import org.spongepowered.asm.mixin.Mixin;
9 | import org.spongepowered.asm.mixin.injection.At;
10 | import org.spongepowered.asm.mixin.injection.Inject;
11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
13 | import org.waste.of.time.Events;
14 |
15 | @Mixin(ClientWorld.class)
16 | public class ClientWorldMixin {
17 |
18 | @Inject(method = "removeEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;onRemoved()V", shift = At.Shift.AFTER))
19 | public void onEntityRemovedInject(final int entityId, final Entity.RemovalReason removalReason, final CallbackInfo ci,
20 | @Local Entity entity) {
21 | Events.INSTANCE.onEntityRemoved(entity, removalReason);
22 | }
23 |
24 | @Inject(method = "getMapState", at = @At("HEAD"))
25 | public void getMapStateInject(MapIdComponent id, CallbackInfoReturnable cir) {
26 | Events.INSTANCE.onMapStateGet(id);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/DebugRendererMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import net.minecraft.client.render.Frustum;
4 | import net.minecraft.client.render.VertexConsumerProvider;
5 | import net.minecraft.client.render.debug.DebugRenderer;
6 | import net.minecraft.client.util.math.MatrixStack;
7 | import org.spongepowered.asm.mixin.Mixin;
8 | import org.spongepowered.asm.mixin.injection.At;
9 | import org.spongepowered.asm.mixin.injection.Inject;
10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
11 | import org.waste.of.time.Events;
12 |
13 | @Mixin(DebugRenderer.class)
14 | public class DebugRendererMixin {
15 | @Inject(method = "render", at = @At("HEAD"))
16 | public void renderInject(
17 | MatrixStack matrices,
18 | Frustum frustum,
19 | VertexConsumerProvider.Immediate vertexConsumers,
20 | double cameraX,
21 | double cameraY,
22 | double cameraZ,
23 | CallbackInfo ci
24 | ) {
25 | Events.INSTANCE.onDebugRenderStart(matrices, vertexConsumers, cameraX, cameraY, cameraZ);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/GameMenuScreenMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import com.llamalad7.mixinextras.sugar.Local;
4 | import net.minecraft.client.gui.screen.GameMenuScreen;
5 | import net.minecraft.client.gui.widget.GridWidget;
6 | import org.spongepowered.asm.mixin.Mixin;
7 | import org.spongepowered.asm.mixin.injection.At;
8 | import org.spongepowered.asm.mixin.injection.Inject;
9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
10 | import org.waste.of.time.Events;
11 |
12 | @Mixin(GameMenuScreen.class)
13 | public class GameMenuScreenMixin {
14 |
15 | @Inject(method = "initWidgets", at = @At(
16 | value = "INVOKE",
17 | target = "Lnet/minecraft/client/MinecraftClient;isInSingleplayer()Z"
18 | ))
19 | public void onInitWidgets(final CallbackInfo ci,
20 | @Local GridWidget.Adder adder) {
21 | Events.INSTANCE.onGameMenuScreenInitWidgets(adder);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/IntegratedServerLoaderMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
4 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
5 | import net.minecraft.server.integrated.IntegratedServerLoader;
6 | import net.minecraft.world.level.storage.LevelStorage;
7 | import org.spongepowered.asm.mixin.Mixin;
8 | import org.spongepowered.asm.mixin.injection.At;
9 | import org.waste.of.time.WorldTools;
10 |
11 | @Mixin(IntegratedServerLoader.class)
12 | public class IntegratedServerLoaderMixin {
13 |
14 | @WrapOperation(method = "checkBackupAndStart",
15 | at = @At(
16 | value = "INVOKE",
17 | target = "Lnet/minecraft/server/integrated/IntegratedServerLoader;showBackupPromptScreen(Lnet/minecraft/world/level/storage/LevelStorage$Session;ZLjava/lang/Runnable;Ljava/lang/Runnable;)V"
18 | ))
19 | public void disableExperimentalWorldSettingsScreen(IntegratedServerLoader instance, LevelStorage.Session session, boolean customized, Runnable callback, Runnable onCancel, Operation original) {
20 | if (WorldTools.INSTANCE.getConfig().getAdvanced().getHideExperimentalWorldGui()) {
21 | callback.run();
22 | } else {
23 | original.call(instance, session, customized, callback, onCancel);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/common/src/main/java/org/waste/of/time/mixin/PalettedContainerMixin.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.mixin;
2 |
3 | import net.minecraft.world.chunk.PalettedContainer;
4 | import org.spongepowered.asm.mixin.Mixin;
5 | import org.spongepowered.asm.mixin.Unique;
6 | import org.spongepowered.asm.mixin.injection.At;
7 | import org.spongepowered.asm.mixin.injection.Inject;
8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
9 | import org.waste.of.time.extension.IPalettedContainerExtension;
10 |
11 | @Mixin(PalettedContainer.class)
12 | public class PalettedContainerMixin implements IPalettedContainerExtension {
13 | @Unique
14 | private boolean ignoreLock = false;
15 |
16 | @Inject(method = "lock", at = @At("HEAD"), cancellable = true)
17 | public void disableChunkContainerLock(CallbackInfo ci) {
18 | if (ignoreLock) ci.cancel();
19 | }
20 |
21 | @Inject(method = "unlock", at = @At("HEAD"), cancellable = true)
22 | public void disableChunkContainerUnLock(CallbackInfo ci) {
23 | if (ignoreLock) ci.cancel();
24 | }
25 |
26 | @Override
27 | public void setWTIgnoreLock(boolean b) {
28 | ignoreLock = b;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/Events.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time
2 |
3 | import net.minecraft.client.MinecraftClient
4 | import net.minecraft.client.gui.screen.Screen
5 | import net.minecraft.client.gui.widget.ButtonWidget
6 | import net.minecraft.client.gui.widget.GridWidget
7 | import net.minecraft.client.render.RenderLayer
8 | import net.minecraft.client.render.VertexConsumer
9 | import net.minecraft.client.render.VertexConsumerProvider
10 | import net.minecraft.client.util.math.MatrixStack
11 | import net.minecraft.component.type.MapIdComponent
12 | import net.minecraft.entity.Entity
13 | import net.minecraft.entity.LivingEntity
14 | import net.minecraft.entity.player.PlayerEntity
15 | import net.minecraft.util.hit.BlockHitResult
16 | import net.minecraft.util.math.BlockPos
17 | import net.minecraft.util.math.Vec3d
18 | import net.minecraft.world.World
19 | import net.minecraft.world.chunk.WorldChunk
20 | import org.waste.of.time.Utils.manhattanDistance2d
21 | import org.waste.of.time.WorldTools.CAPTURE_KEY
22 | import org.waste.of.time.WorldTools.CONFIG_KEY
23 | import org.waste.of.time.WorldTools.config
24 | import org.waste.of.time.WorldTools.mc
25 | import org.waste.of.time.gui.ManagerScreen
26 | import org.waste.of.time.manager.BarManager.updateCapture
27 | import org.waste.of.time.manager.CaptureManager
28 | import org.waste.of.time.manager.CaptureManager.capturing
29 | import org.waste.of.time.manager.CaptureManager.currentLevelName
30 | import org.waste.of.time.manager.MessageManager
31 | import org.waste.of.time.manager.MessageManager.translateHighlight
32 | import org.waste.of.time.manager.StatisticManager
33 | import org.waste.of.time.storage.StorageFlow
34 | import org.waste.of.time.storage.cache.EntityCacheable
35 | import org.waste.of.time.storage.cache.HotCache
36 | import org.waste.of.time.storage.cache.DataInjectionHandler
37 | import org.waste.of.time.storage.serializable.BlockEntityLoadable
38 | import org.waste.of.time.storage.serializable.PlayerStoreable
39 | import org.waste.of.time.storage.serializable.RegionBasedChunk
40 | import java.awt.Color
41 |
42 | object Events {
43 | fun onChunkLoad(chunk: WorldChunk) {
44 | if (!capturing) return
45 | RegionBasedChunk(chunk).cache()
46 | BlockEntityLoadable(chunk).emit()
47 | }
48 |
49 | fun onChunkUnload(chunk: WorldChunk) {
50 | if (!capturing) return
51 | (HotCache.chunks[chunk.pos] ?: RegionBasedChunk(chunk)).apply {
52 | emit()
53 | flush()
54 | }
55 | }
56 |
57 | fun onEntityLoad(entity: Entity) {
58 | if (!capturing) return
59 | if (entity is PlayerEntity) {
60 | PlayerStoreable(entity).cache()
61 | } else {
62 | EntityCacheable(entity).cache()
63 | }
64 | }
65 |
66 | fun onEntityUnload(entity: Entity) {
67 | if (!capturing) return
68 | if (entity !is PlayerEntity) return
69 | PlayerStoreable(entity).apply {
70 | emit()
71 | flush()
72 | }
73 | }
74 |
75 | fun onClientTickStart() {
76 | if (CAPTURE_KEY.wasPressed() && mc.world != null && mc.currentScreen == null) {
77 | CaptureManager.toggleCapture()
78 | }
79 |
80 | if (CONFIG_KEY.wasPressed() && mc.world != null && mc.currentScreen == null) {
81 | mc.setScreen(ManagerScreen)
82 | }
83 |
84 | if (!capturing) return
85 | updateCapture()
86 | }
87 |
88 | fun onClientJoin() {
89 | HotCache.clear()
90 | StorageFlow.lastStored = null
91 | StatisticManager.reset()
92 | if (config.general.autoDownload) CaptureManager.start()
93 | }
94 |
95 | fun onClientDisconnect() {
96 | if (!capturing) return
97 | CaptureManager.stop()
98 | }
99 |
100 | fun onInteractBlock(world: World, hitResult: BlockHitResult) {
101 | if (!capturing) return
102 | val blockEntity = world.getBlockEntity(hitResult.blockPos)
103 | HotCache.lastInteractedBlockEntity = blockEntity
104 | HotCache.lastInteractedEntity = null
105 | }
106 |
107 | fun onInteractEntity(entity: Entity) {
108 | if (!capturing) return
109 | HotCache.lastInteractedEntity = entity
110 | HotCache.lastInteractedBlockEntity = null
111 | }
112 |
113 | fun onDebugRenderStart(
114 | matrices: MatrixStack,
115 | vertexConsumers: VertexConsumerProvider.Immediate,
116 | cameraX: Double,
117 | cameraY: Double,
118 | cameraZ: Double
119 | ) {
120 | if (!capturing || !config.render.renderNotYetCachedContainers) return
121 |
122 | val vertexConsumer = vertexConsumers.getBuffer(RenderLayer.getLines()) ?: return
123 |
124 | HotCache.unscannedBlockEntities
125 | .forEach { render(it.pos.vec, cameraX, cameraY, cameraZ, matrices, vertexConsumer, Color(config.render.unscannedContainerColor)) }
126 |
127 | HotCache.loadedBlockEntities
128 | .forEach { render(it.value.pos.vec, cameraX, cameraY, cameraZ, matrices, vertexConsumer, Color(config.render.fromCacheLoadedContainerColor)) }
129 |
130 | HotCache.unscannedEntities
131 | .forEach { render(it.entity.pos.add(-.5, .0, -.5), cameraX, cameraY, cameraZ, matrices, vertexConsumer, Color(config.render.unscannedEntityColor)) }
132 | }
133 |
134 | private val BlockPos.vec get() = Vec3d(x.toDouble(), y.toDouble(), z.toDouble())
135 |
136 | private fun render(
137 | vec: Vec3d,
138 | cameraX: Double,
139 | cameraY: Double,
140 | cameraZ: Double,
141 | matrices: MatrixStack,
142 | vertexConsumer: VertexConsumer,
143 | color: Color
144 | ) {
145 | val x1 = (vec.x - cameraX).toFloat()
146 | val y1 = (vec.y - cameraY).toFloat()
147 | val z1 = (vec.z - cameraZ).toFloat()
148 | val x2 = x1 + 1
149 | val z2 = z1 + 1
150 | val r = color.red / 255.0f
151 | val g = color.green / 255.0f
152 | val b = color.blue / 255.0f
153 | val a = 1.0f
154 | val positionMat = matrices.peek().positionMatrix
155 | val normMat = matrices.peek()
156 | vertexConsumer.vertex(positionMat, x1, y1, z1).color(r, g, b, a).normal(normMat, 1.0f, 0.0f, 0.0f)
157 | vertexConsumer.vertex(positionMat, x2, y1, z1).color(r, g, b, a).normal(normMat, 1.0f, 0.0f, 0.0f)
158 | vertexConsumer.vertex(positionMat, x1, y1, z1).color(r, g, b, a).normal(normMat, 0.0f, 0.0f, 1.0f)
159 | vertexConsumer.vertex(positionMat, x1, y1, z2).color(r, g, b, a).normal(normMat, 0.0f, 0.0f, 1.0f)
160 | vertexConsumer.vertex(positionMat, x1, y1, z2).color(r, g, b, a).normal(normMat, 1.0f, 0.0f, 0.0f)
161 | vertexConsumer.vertex(positionMat, x2, y1, z2).color(r, g, b, a).normal(normMat, 1.0f, 0.0f, 0.0f)
162 | vertexConsumer.vertex(positionMat, x2, y1, z2).color(r, g, b, a).normal(normMat, 0.0f, 0.0f, -1.0f)
163 | vertexConsumer.vertex(positionMat, x2, y1, z1).color(r, g, b, a).normal(normMat, 0.0f, 0.0f, -1.0f)
164 | }
165 |
166 | fun onGameMenuScreenInitWidgets(adder: GridWidget.Adder) {
167 | val widget = if (capturing) {
168 | val label = translateHighlight("worldtools.gui.escape.button.finish_download", currentLevelName)
169 | ButtonWidget.builder(label) {
170 | CaptureManager.stop()
171 | mc.setScreen(null)
172 | }.width(204).build()
173 | } else {
174 | ButtonWidget.builder(MessageManager.brand) {
175 | MinecraftClient.getInstance().setScreen(ManagerScreen)
176 | }.width(204).build()
177 | }
178 |
179 | adder.add(widget, 2)
180 | }
181 |
182 | fun onScreenRemoved(screen: Screen) {
183 | if (!capturing) return
184 | DataInjectionHandler.onScreenRemoved(screen)
185 | HotCache.lastInteractedBlockEntity = null
186 | }
187 |
188 | fun onEntityRemoved(entity: Entity, reason: Entity.RemovalReason) {
189 | if (!capturing) return
190 | if (reason != Entity.RemovalReason.KILLED && reason != Entity.RemovalReason.DISCARDED) return
191 |
192 | if (entity is LivingEntity) {
193 | if (!entity.isDead) return
194 |
195 | val cacheable = EntityCacheable(entity)
196 | HotCache.entities.entries.find { (_, entities) ->
197 | entities.contains(cacheable)
198 | }?.value?.remove(cacheable)
199 | } else {
200 | // todo: its actually a bit tricky to differentiate the entity being removed from our world or the server world
201 | // need to find a reliable way to determine it
202 | // if chunk is loaded, remove the entity? -> doesn't seem to work because server will remove entity before chunk is unloaded
203 | mc.player?.let { player ->
204 | if (entity.pos.manhattanDistance2d(player.pos) < 32) { // todo: configurable distance, this should be small enough to be safe for most cases
205 | val cacheable = EntityCacheable(entity)
206 | HotCache.entities[entity.chunkPos]?.remove(cacheable)
207 | }
208 | }
209 | }
210 | }
211 |
212 | fun onMapStateGet(id: MapIdComponent) {
213 | if (!capturing) return
214 | // todo: looks like the server does not send a map update packet for container
215 | HotCache.mapIDs.add(id.id)
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/Utils.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time
2 |
3 | import net.minecraft.block.entity.BlockEntity
4 | import net.minecraft.registry.Registries
5 | import net.minecraft.util.math.Vec3d
6 | import java.time.LocalDateTime
7 | import java.time.ZoneId
8 | import java.time.ZonedDateTime
9 | import java.time.format.DateTimeFormatter
10 | import kotlin.math.abs
11 | import kotlin.math.ln
12 | import kotlin.math.pow
13 |
14 | object Utils {
15 | // Why cant I use the std lib?
16 | fun Boolean.toByte(): Byte = if (this) 1 else 0
17 | fun Vec3d.asString() = "(%.2f, %.2f, %.2f)".format(x, y, z)
18 |
19 | fun getTime(): String {
20 | val localDateTime = LocalDateTime.now()
21 | val zoneId = ZoneId.systemDefault()
22 |
23 | val zonedDateTime = ZonedDateTime.of(localDateTime, zoneId)
24 | val formatter = DateTimeFormatter.RFC_1123_DATE_TIME
25 |
26 | return zonedDateTime.format(formatter)
27 | }
28 |
29 | fun Vec3d.manhattanDistance2d(other: Vec3d) =
30 | abs(this.x - other.x) + abs(this.z - other.z)
31 |
32 | fun Long.toReadableByteCount(si: Boolean = true): String {
33 | val unit = if (si) 1000 else 1024
34 | if (this < unit) return "$this B"
35 | val exp = (ln(toDouble()) / ln(unit.toDouble())).toInt()
36 | val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
37 | return String.format("%.1f %sB", this / unit.toDouble().pow(exp.toDouble()), pre)
38 | }
39 |
40 | val BlockEntity.typeName: String
41 | get() = Registries.BLOCK_ENTITY_TYPE.getId(type)?.path ?: "unknown"
42 | }
43 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/WorldTools.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import me.shedaniel.autoconfig.AutoConfig
6 | import me.shedaniel.autoconfig.serializer.GsonConfigSerializer
7 | import net.minecraft.SharedConstants
8 | import net.minecraft.client.MinecraftClient
9 | import net.minecraft.client.option.KeyBinding
10 | import net.minecraft.client.util.InputUtil
11 | import org.apache.logging.log4j.LogManager
12 | import org.apache.logging.log4j.Logger
13 | import org.lwjgl.glfw.GLFW
14 | import org.waste.of.time.config.WorldToolsConfig
15 |
16 | object WorldTools {
17 | const val MOD_NAME = "WorldTools"
18 | const val MOD_ID = "worldtools"
19 | private const val URL = "https://github.com/Avanatiker/WorldTools/"
20 | const val MCA_EXTENSION = ".mca"
21 | const val DAT_EXTENSION = ".dat"
22 | const val MAX_LEVEL_NAME_LENGTH = 64
23 | const val TIMESTAMP_KEY = "CaptureTimestamp"
24 | val GSON: Gson = GsonBuilder().setPrettyPrinting().create()
25 | val CURRENT_VERSION = SharedConstants.getGameVersion().saveVersion.id
26 | private val VERSION: String = LoaderInfo.getVersion()
27 | val CREDIT_MESSAGE = "This file was created by $MOD_NAME $VERSION ($URL)"
28 | val CREDIT_MESSAGE_MD = "This file was created by [$MOD_NAME $VERSION]($URL)"
29 | val LOG: Logger = LogManager.getLogger()
30 | var CAPTURE_KEY = KeyBinding(
31 | "$MOD_ID.key.toggle_capture", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_F12,
32 | "$MOD_ID.key.categories"
33 | )
34 | var CONFIG_KEY = KeyBinding(
35 | "$MOD_ID.key.open_config", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_F10,
36 | "$MOD_ID.key.categories"
37 | )
38 |
39 | val mc: MinecraftClient = MinecraftClient.getInstance()
40 | lateinit var config: WorldToolsConfig; private set
41 |
42 | fun initialize() {
43 | LOG.info("Initializing $MOD_NAME $VERSION")
44 | AutoConfig.register(WorldToolsConfig::class.java, ::GsonConfigSerializer)
45 | config = AutoConfig.getConfigHolder(WorldToolsConfig::class.java).config
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/config/WorldToolsConfig.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.config
2 |
3 | import me.shedaniel.autoconfig.ConfigData
4 | import me.shedaniel.autoconfig.annotation.Config
5 | import me.shedaniel.autoconfig.annotation.ConfigEntry
6 | import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.TransitiveObject
7 | import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.CollapsibleObject
8 | import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.Tooltip
9 | import me.shedaniel.autoconfig.annotation.ConfigEntry.Category
10 | import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.Excluded
11 | import net.minecraft.entity.boss.BossBar
12 |
13 | /**
14 | * See [Cloth Config Documentation](https://shedaniel.gitbook.io/cloth-config/auto-config/creating-a-config-class)
15 | * for more information on how to create a config class
16 | */
17 | @Config(name = "worldtools")
18 | class WorldToolsConfig : ConfigData {
19 | @TransitiveObject
20 | @Category("General")
21 | val general = General()
22 |
23 | @TransitiveObject
24 | @Category("World")
25 | val world = World()
26 |
27 | @TransitiveObject
28 | @Category("Entity")
29 | val entity = Entity()
30 |
31 | @TransitiveObject
32 | @Category("Render")
33 | val render = Render()
34 |
35 | @TransitiveObject
36 | @Category("Advanced")
37 | val advanced = Advanced()
38 |
39 | @TransitiveObject
40 | @Category("Debug")
41 | @ConfigEntry.Gui.PrefixText
42 | val debug = Debug()
43 |
44 | class General {
45 | @Tooltip
46 | var autoDownload = false
47 | @Tooltip
48 | var compressLevel = true
49 | @Excluded
50 | var reloadBlockEntities = true
51 |
52 | @CollapsibleObject(startExpanded = true)
53 | @Tooltip
54 | var capture = Capture()
55 |
56 | class Capture {
57 | var chunks = true
58 | var entities = true
59 | var players = true
60 | var statistics = true
61 | var levelData = true
62 | var advancements = true
63 | var metadata = true
64 | var maps = true
65 | }
66 | }
67 |
68 | class World {
69 | @CollapsibleObject(startExpanded = true)
70 | val worldGenerator = WorldGenerator()
71 |
72 | @CollapsibleObject(startExpanded = true)
73 | val gameRules = GameRules()
74 |
75 | @CollapsibleObject(startExpanded = true)
76 | val metadata = Metadata()
77 |
78 | @CollapsibleObject(startExpanded = true)
79 | @Excluded
80 | val censor = Censor()
81 |
82 | class WorldGenerator {
83 | @Tooltip
84 | var type = GeneratorType.VOID
85 | val bonusChest = false
86 | val generateFeatures = false
87 | var seed = 0L
88 |
89 | enum class GeneratorType {
90 | VOID,
91 | DEFAULT,
92 | FLAT
93 | }
94 | }
95 |
96 | class GameRules {
97 | @Tooltip
98 | var modifyGameRules = true
99 |
100 | var doWardenSpawning = false
101 | var doFireTick = false
102 | var doVinesSpread = false
103 | var doMobSpawning = false
104 | var doDaylightCycle = false
105 | var doWeatherCycle = false
106 | var keepInventory = true
107 | var doMobGriefing = false
108 | var doTraderSpawning = false
109 | var doPatrolSpawning = false
110 | }
111 |
112 | class Metadata {
113 | var captureTimestamp = true
114 | }
115 |
116 | class Censor {
117 | // ToDo: Add censoring options
118 | }
119 | }
120 |
121 | class Entity {
122 | @CollapsibleObject(startExpanded = true)
123 | val behavior = Behavior()
124 |
125 | @CollapsibleObject(startExpanded = true)
126 | val metadata = Metadata()
127 |
128 | @CollapsibleObject(startExpanded = true)
129 | val censor = Censor()
130 |
131 | class Behavior {
132 | @Tooltip
133 | var modifyEntityBehavior = false
134 |
135 | var noAI = true
136 | var noGravity = true
137 | var invulnerable = true
138 | var silent = true
139 | }
140 |
141 | class Metadata {
142 | var captureTimestamp = true
143 | }
144 |
145 | class Censor {
146 | // ToDo: Add censoring options
147 |
148 | @Excluded
149 | var names = false
150 | @Excluded
151 | var owner = false
152 | @Tooltip
153 | var lastDeathLocation = true
154 | }
155 | }
156 |
157 | class Render {
158 | var renderNotYetCachedContainers = true
159 | @ConfigEntry.ColorPicker
160 | var unscannedContainerColor = 0xDE0000
161 | @ConfigEntry.ColorPicker
162 | var fromCacheLoadedContainerColor = 0xFFA500
163 | @ConfigEntry.ColorPicker
164 | var unscannedEntityColor = 0xDE0000
165 | @ConfigEntry.ColorPicker
166 | var fromCacheLoadedEntityColor = 0xFFA500
167 | @ConfigEntry.ColorPicker
168 | var accentColor = 0xA2FF4C
169 | var captureBarColor = BossBar.Color.PINK
170 | var captureBarStyle = BossBar.Style.NOTCHED_10
171 | var progressBarColor = BossBar.Color.GREEN
172 | var progressBarStyle = BossBar.Style.PROGRESS
173 | @ConfigEntry.BoundedDiscrete(min = 50, max = 60000)
174 | var progressBarTimeout = 3000L
175 | }
176 |
177 | class Advanced {
178 | @Tooltip
179 | var anonymousMode = false
180 | @Tooltip
181 | var hideExperimentalWorldGui = true // See IntegratedServerLoaderMixin
182 | var showToasts = true
183 | var showChatMessages = true
184 | var keepEnderChestContents = false
185 | }
186 |
187 | class Debug {
188 | var logSettings = false
189 | var logSavedChunks = false
190 | var logSavedEntities = false
191 | var logSavedContainers = false
192 | var logSavedMaps = false
193 | var logZippingProgress = false
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/gui/BrowseDownloadsScreen.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.gui
2 |
3 | import net.minecraft.client.MinecraftClient
4 | import net.minecraft.client.gui.DrawContext
5 | import net.minecraft.client.gui.screen.Screen
6 | import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget
7 | import net.minecraft.text.Text
8 |
9 | // todo: we need a local database of downloads like original wdl mod
10 | object BrowseDownloadsScreen : Screen(Text.translatable("worldtools.gui.browser.title")) {
11 | override fun init() {
12 |
13 | }
14 |
15 | object DownloadListWidget : AlwaysSelectedEntryListWidget(
16 | MinecraftClient.getInstance(),
17 | width,
18 | height,
19 | 20,
20 | height - 30
21 | ) {
22 |
23 |
24 | }
25 |
26 | class WorldDownloadEntry : AlwaysSelectedEntryListWidget.Entry() {
27 | override fun render(
28 | context: DrawContext?,
29 | index: Int,
30 | y: Int,
31 | x: Int,
32 | entryWidth: Int,
33 | entryHeight: Int,
34 | mouseX: Int,
35 | mouseY: Int,
36 | hovered: Boolean,
37 | tickDelta: Float
38 | ) {
39 | TODO("Not yet implemented")
40 | }
41 |
42 | override fun getNarration(): Text {
43 | TODO("Not yet implemented")
44 | }
45 |
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/gui/EnterTextField.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.gui
2 |
3 | import net.minecraft.client.MinecraftClient
4 | import net.minecraft.client.font.TextRenderer
5 | import net.minecraft.client.gui.widget.TextFieldWidget
6 | import net.minecraft.text.Text
7 | import org.lwjgl.glfw.GLFW
8 | import org.waste.of.time.manager.CaptureManager
9 |
10 | class EnterTextField(
11 | textRenderer: TextRenderer, x: Int, y: Int, width: Int, height: Int, message: Text, val client: MinecraftClient?
12 | ) : TextFieldWidget(textRenderer, x, y, width, height, message) {
13 |
14 | override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
15 | if (keyCode == GLFW.GLFW_KEY_ENTER) {
16 | if (CaptureManager.capturing) {
17 | client?.setScreen(null)
18 | CaptureManager.stop()
19 | } else {
20 | client?.setScreen(null)
21 | CaptureManager.start(text)
22 | }
23 | return true
24 | }
25 | return super.keyPressed(keyCode, scanCode, modifiers)
26 | }
27 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/gui/ManagerScreen.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.gui
2 |
3 | import me.shedaniel.autoconfig.AutoConfig
4 | import net.minecraft.client.gui.screen.Screen
5 | import net.minecraft.client.gui.widget.*
6 | import net.minecraft.text.Text
7 | import org.waste.of.time.WorldTools.MAX_LEVEL_NAME_LENGTH
8 | import org.waste.of.time.config.WorldToolsConfig
9 | import org.waste.of.time.manager.CaptureManager
10 | import org.waste.of.time.manager.CaptureManager.currentLevelName
11 | import org.waste.of.time.manager.CaptureManager.levelName
12 |
13 | object ManagerScreen : Screen(Text.translatable("worldtools.gui.manager.title")) {
14 | private lateinit var worldNameTextEntryWidget: TextFieldWidget
15 | private lateinit var titleWidget: TextWidget
16 | private lateinit var downloadButton: ButtonWidget
17 | private lateinit var configButton: ButtonWidget
18 | private lateinit var cancelButton: ButtonWidget
19 | private const val BUTTON_WIDTH = 90
20 |
21 | override fun init() {
22 | setupTitle()
23 | setupEntryGrid()
24 | setupBottomGrid()
25 | }
26 |
27 | override fun tick() {
28 | if (CaptureManager.capturing) {
29 | downloadButton.message = Text.translatable("worldtools.gui.manager.button.stop_download")
30 | worldNameTextEntryWidget.setPlaceholder(Text.of(currentLevelName))
31 | worldNameTextEntryWidget.setEditable(false)
32 | } else {
33 | downloadButton.message = Text.translatable("worldtools.gui.manager.button.start_download")
34 | worldNameTextEntryWidget.setEditable(true)
35 | }
36 | super.tick()
37 | }
38 |
39 | private fun setupTitle() {
40 | titleWidget = TextWidget(Text.translatable("worldtools.gui.manager.title"), textRenderer)
41 | SimplePositioningWidget.setPos(titleWidget, 0, 0, width, height, 0.5f, 0.01f)
42 | addDrawableChild(titleWidget)
43 | }
44 |
45 | private fun setupEntryGrid() {
46 | val entryGridWidget = createGridWidget()
47 | val adder = entryGridWidget.createAdder(3)
48 |
49 | worldNameTextEntryWidget = EnterTextField(
50 | textRenderer, 0, 0, 250, 20, Text.of(levelName), client
51 | ).apply {
52 | setPlaceholder(Text.translatable("worldtools.gui.manager.world_name_placeholder", levelName))
53 | setMaxLength(MAX_LEVEL_NAME_LENGTH)
54 | }
55 | downloadButton = createButton("worldtools.gui.manager.button.start_download") {
56 | if (CaptureManager.capturing) {
57 | client?.setScreen(null)
58 | CaptureManager.stop()
59 | } else {
60 | client?.setScreen(null)
61 | CaptureManager.start(worldNameTextEntryWidget.text)
62 | }
63 | }
64 |
65 | adder.add(worldNameTextEntryWidget, 2)
66 | adder.add(downloadButton, 1)
67 |
68 | entryGridWidget.refreshPositions()
69 | SimplePositioningWidget.setPos(entryGridWidget, 0, titleWidget.y, width, height, 0.5f, 0.05f)
70 | entryGridWidget.forEachChild(this::addDrawableChild)
71 | }
72 |
73 | private fun setupBottomGrid() {
74 | val bottomGridWidget = createGridWidget()
75 | val bottomAdder = bottomGridWidget.createAdder(2)
76 | configButton = createButton("worldtools.gui.manager.button.config") {
77 | client?.setScreen(AutoConfig.getConfigScreen(WorldToolsConfig::class.java, this).get())
78 | }
79 | cancelButton = createButton("worldtools.gui.manager.button.cancel") {
80 | client?.setScreen(null)
81 | }
82 |
83 | bottomAdder.add(configButton, 1)
84 | bottomAdder.add(cancelButton, 1)
85 |
86 | bottomGridWidget.refreshPositions()
87 | SimplePositioningWidget.setPos(bottomGridWidget, 0, 0, width, height, 0.5f, .95f)
88 | bottomGridWidget.forEachChild(this::addDrawableChild)
89 | }
90 |
91 | private fun createGridWidget() = GridWidget().apply {
92 | mainPositioner.margin(4, 4, 4, 4)
93 | }
94 |
95 | private fun createButton(textKey: String, onClick: (ButtonWidget) -> Unit) =
96 | ButtonWidget.Builder(Text.translatable(textKey), onClick).width(BUTTON_WIDTH).build()
97 | }
98 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/manager/BarManager.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.manager
2 |
3 |
4 | import net.minecraft.client.gui.hud.ClientBossBar
5 | import net.minecraft.text.Text
6 | import org.waste.of.time.manager.CaptureManager.capturing
7 | import org.waste.of.time.WorldTools.config
8 | import org.waste.of.time.manager.MessageManager.info
9 | import org.waste.of.time.storage.StorageFlow
10 | import java.util.*
11 |
12 | object BarManager {
13 |
14 | val progressBar =
15 | ClientBossBar(
16 | UUID.randomUUID(),
17 | Text.of(""),
18 | 0f,
19 | config.render.progressBarColor,
20 | config.render.progressBarStyle,
21 | false,
22 | false,
23 | false
24 | )
25 |
26 | private val captureInfoBar =
27 | ClientBossBar(
28 | UUID.randomUUID(),
29 | Text.of(""),
30 | 1.0f,
31 | config.render.captureBarColor,
32 | config.render.captureBarStyle,
33 | false,
34 | false,
35 | false
36 | )
37 |
38 | fun progressBar() = if (StorageFlow.lastStored != null) {
39 | Optional.of(progressBar)
40 | } else {
41 | Optional.empty()
42 | }
43 |
44 | fun getCaptureBar() = if (capturing) {
45 | Optional.of(captureInfoBar)
46 | } else {
47 | Optional.empty()
48 | }
49 |
50 | fun updateCapture() {
51 | captureInfoBar.name = StatisticManager.infoMessage
52 | captureInfoBar.color = config.render.captureBarColor
53 | captureInfoBar.style = config.render.captureBarStyle
54 | progressBar.color = config.render.progressBarColor
55 | progressBar.percent = 0f
56 |
57 | StorageFlow.lastStored?.let {
58 | val elapsed = System.currentTimeMillis() - StorageFlow.lastStoredTimestamp
59 | val timeout = config.render.progressBarTimeout
60 | val progress = (elapsed.toFloat() / timeout).coerceAtMost(1f)
61 |
62 | progressBar.percent = progress
63 | progressBar.name = it.formattedInfo
64 |
65 | if (elapsed >= timeout) {
66 | StorageFlow.lastStored = null
67 | progressBar.percent = 0f
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/manager/CaptureManager.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.manager
2 |
3 | import kotlinx.coroutines.*
4 | import net.minecraft.client.gui.screen.ConfirmScreen
5 | import net.minecraft.client.network.ClientPlayerEntity
6 | import net.minecraft.entity.player.PlayerEntity
7 | import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket
8 | import net.minecraft.registry.RegistryKey
9 | import net.minecraft.registry.RegistryKeys
10 | import net.minecraft.text.Text
11 | import net.minecraft.world.World
12 | import org.waste.of.time.WorldTools
13 | import org.waste.of.time.WorldTools.LOG
14 | import org.waste.of.time.WorldTools.config
15 | import org.waste.of.time.WorldTools.mc
16 | import org.waste.of.time.config.WorldToolsConfig
17 | import org.waste.of.time.manager.MessageManager.info
18 | import org.waste.of.time.storage.StorageFlow
19 | import org.waste.of.time.storage.cache.EntityCacheable
20 | import org.waste.of.time.storage.cache.HotCache
21 | import org.waste.of.time.storage.serializable.*
22 |
23 | object CaptureManager {
24 | private const val MAX_WORLD_NAME_LENGTH = 64
25 | var capturing = false
26 | private var storeJob: Job? = null
27 | var currentLevelName: String = "Not yet initialized"
28 | var lastPlayer: ClientPlayerEntity? = null
29 | var lastWorldKeys = mutableSetOf>()
30 |
31 | val levelName: String
32 | get() = if (mc.isInSingleplayer) {
33 | mc.server?.serverMotd?.substringAfter(" - ")?.sanitizeWorldName() ?: "Singleplayer"
34 | } else {
35 | mc.networkHandler?.serverInfo?.address?.sanitizeWorldName() ?: "Multiplayer"
36 | }
37 |
38 | fun toggleCapture() {
39 | if (capturing) stop() else start()
40 | }
41 |
42 | fun start(customName: String? = null, confirmed: Boolean = false) {
43 | if (capturing) {
44 | MessageManager.sendError("worldtools.log.error.already_capturing", currentLevelName)
45 | return
46 | }
47 |
48 | if (mc.isInSingleplayer) {
49 | MessageManager.sendInfo("worldtools.log.info.singleplayer_capture")
50 | }
51 |
52 | val potentialName = customName?.let { potentialName ->
53 | if (potentialName.length > MAX_WORLD_NAME_LENGTH) {
54 | MessageManager.sendError(
55 | "worldtools.log.error.world_name_too_long",
56 | potentialName,
57 | MAX_WORLD_NAME_LENGTH
58 | )
59 | return
60 | }
61 |
62 | potentialName.ifBlank { levelName }
63 | } ?: levelName
64 |
65 | val worldExists = mc.levelStorage.savesDirectory.resolve(potentialName).toFile().exists()
66 | if (worldExists && !confirmed) {
67 | mc.setScreen(ConfirmScreen(
68 | { yes ->
69 | if (yes) start(potentialName, true)
70 | mc.setScreen(null)
71 | },
72 | Text.translatable("worldtools.gui.capture.existing_world_confirm.title"),
73 | Text.translatable("worldtools.gui.capture.existing_world_confirm.message", potentialName)
74 | ))
75 | return
76 | }
77 |
78 | HotCache.clear()
79 | currentLevelName = potentialName
80 | lastPlayer = mc.player
81 | lastWorldKeys.addAll(mc.networkHandler?.worldKeys ?: emptySet())
82 | MessageManager.sendInfo("worldtools.log.info.started_capture", potentialName)
83 | if (config.debug.logSettings) logCaptureSettingsState()
84 | storeJob = StorageFlow.launch(potentialName)
85 | mc.networkHandler?.sendPacket(ClientStatusC2SPacket(ClientStatusC2SPacket.Mode.REQUEST_STATS))
86 | capturing = true
87 |
88 | // Need to wait until the storage flow is running before syncing the cache
89 | CoroutineScope(Dispatchers.IO).launch {
90 | delay(100L)
91 | mc.execute {
92 | syncCacheFromWorldState()
93 | }
94 | }
95 | }
96 |
97 | private fun logCaptureSettingsState() {
98 | WorldTools.GSON.toJson(config, WorldToolsConfig::class.java).let { configJson ->
99 | LOG.info("Launching capture with settings:")
100 | LOG.info(configJson)
101 | }
102 | }
103 |
104 | fun stop() {
105 | if (!capturing) {
106 | MessageManager.sendError("worldtools.log.error.not_capturing")
107 | return
108 | }
109 |
110 | MessageManager.sendInfo("worldtools.log.info.stopping_capture", currentLevelName)
111 |
112 | HotCache.chunks.values.forEach { chunk ->
113 | chunk.emit() // will also write entities in the chunks
114 | }
115 |
116 | HotCache.players.forEach { player ->
117 | player.emit()
118 | }
119 |
120 | MapDataStoreable().emit()
121 | LevelDataStoreable().emit()
122 | AdvancementsStoreable().emit()
123 | MetadataStoreable().emit()
124 | CompressLevelStoreable().emit()
125 | EndFlow().emit()
126 | }
127 |
128 | private fun syncCacheFromWorldState() {
129 | val world = mc.world ?: return
130 | val diameter = world.chunkManager.chunks.diameter
131 |
132 | repeat(diameter * diameter) { i ->
133 | world.chunkManager.chunks.getChunk(i)?.let { chunk ->
134 | RegionBasedChunk(chunk).cache()
135 | BlockEntityLoadable(chunk).emit()
136 | }
137 | }
138 |
139 | world.entities.forEach {
140 | if (it is PlayerEntity) {
141 | PlayerStoreable(it).cache()
142 | } else {
143 | EntityCacheable(it).cache()
144 | }
145 | }
146 | }
147 |
148 | private fun String.sanitizeWorldName() = replace(":", "_")
149 | }
150 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/manager/MessageManager.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.manager
2 |
3 | import net.minecraft.client.toast.SystemToast
4 | import net.minecraft.client.toast.Toast
5 | import net.minecraft.text.MutableText
6 | import net.minecraft.text.Text
7 | import net.minecraft.text.TextColor
8 | import org.waste.of.time.WorldTools.LOG
9 | import org.waste.of.time.WorldTools.config
10 | import org.waste.of.time.WorldTools.mc
11 |
12 | object MessageManager {
13 | private const val ERROR_COLOR = 0xff3333
14 |
15 | val brand: Text = Text.empty()
16 | .append(
17 | Text.literal("W").styled {
18 | it.withColor(TextColor.fromRgb(config.render.accentColor))
19 | }
20 | ).append(
21 | Text.literal("orld")
22 | ).append(
23 | Text.literal("T").styled {
24 | it.withColor(TextColor.fromRgb(config.render.accentColor))
25 | }
26 | ).append(
27 | Text.literal("ools")
28 | )
29 | private val converted by lazy {
30 | Text.literal("[").append(brand).append(Text.of("] "))
31 | }
32 | private val fullBrand: MutableText
33 | get() = converted.copy()
34 |
35 | fun String.info() =
36 | Text.of(this).sendInfo()
37 |
38 | fun sendInfo(translateKey: String, vararg args: Any) = translateHighlight(translateKey, *args).sendInfo()
39 |
40 | fun sendError(translateKey: String, vararg args: Any) = Text.translatable(translateKey, *args).sendError()
41 |
42 | fun Text.infoToast() {
43 | SystemToast.create(
44 | mc,
45 | SystemToast.Type.WORLD_BACKUP,
46 | brand,
47 | this
48 | ).addToast()
49 | }
50 |
51 | private fun Text.errorToast() {
52 | SystemToast.create(
53 | mc,
54 | SystemToast.Type.WORLD_ACCESS_FAILURE,
55 | brand,
56 | this
57 | ).addToast()
58 | }
59 |
60 | fun Text.sendInfo() =
61 | fullBrand.append(this).addMessage()
62 |
63 | private fun Text.sendError() {
64 | LOG.error(string)
65 | val errorText = copy().styled {
66 | it.withColor(ERROR_COLOR)
67 | }
68 |
69 | fullBrand.append(errorText).addMessage()
70 | errorText.errorToast()
71 | }
72 |
73 | private fun Text.addMessage() {
74 | if (!config.advanced.showChatMessages) return
75 |
76 | mc.execute {
77 | mc.inGameHud.chatHud.addMessage(this)
78 | }
79 | }
80 |
81 | private fun Toast.addToast() {
82 | if (!config.advanced.showToasts) return
83 |
84 | mc.execute {
85 | mc.toastManager.add(this)
86 | }
87 | }
88 |
89 | fun translateHighlight(key: String, vararg args: Any): MutableText =
90 | args.map { element ->
91 | val secondaryColor = TextColor.fromRgb(config.render.accentColor)
92 | if (element is Text) {
93 | if (element.style.color != null) {
94 | element
95 | } else {
96 | element.copy().styled { style ->
97 | style.withColor(secondaryColor)
98 | }
99 | }
100 | } else {
101 | Text.literal(element.toString()).styled { style ->
102 | style.withColor(secondaryColor)
103 | }
104 | }
105 | }.toTypedArray().let {
106 | Text.translatable(key, *it)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/manager/StatisticManager.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.manager
2 |
3 | import net.minecraft.text.MutableText
4 | import net.minecraft.text.Text
5 | import net.minecraft.text.TextColor
6 | import org.waste.of.time.manager.CaptureManager.currentLevelName
7 | import org.waste.of.time.manager.MessageManager.translateHighlight
8 | import org.waste.of.time.WorldTools.config
9 | import org.waste.of.time.storage.cache.HotCache
10 |
11 | object StatisticManager {
12 | var chunks = 0
13 | var entities = 0
14 | var players = 0
15 | private val containers get(): Int =
16 | HotCache.scannedBlockEntities.size + HotCache.loadedBlockEntities.size
17 | val dimensions = mutableSetOf()
18 |
19 | fun reset() {
20 | chunks = 0
21 | entities = 0
22 | players = 0
23 | dimensions.clear()
24 | }
25 |
26 | val infoMessage: Text
27 | get() {
28 | val savedElements = mutableListOf().apply {
29 | if (chunks == 1) {
30 | add(translateHighlight("worldtools.capture.chunk", chunks))
31 | }
32 | if (chunks > 1) {
33 | add(translateHighlight("worldtools.capture.chunks", "%,d".format(chunks)))
34 | }
35 |
36 | if (entities == 1) {
37 | add(translateHighlight("worldtools.capture.entity", entities))
38 | }
39 | if (entities > 1) {
40 | add(translateHighlight("worldtools.capture.entities", "%,d".format(entities)))
41 | }
42 |
43 | if (players == 1) {
44 | add(translateHighlight("worldtools.capture.player", players))
45 | }
46 | if (players > 1) {
47 | add(translateHighlight("worldtools.capture.players", "%,d".format(players)))
48 | }
49 |
50 | if (containers == 1) {
51 | add(translateHighlight("worldtools.capture.container", containers))
52 | }
53 | if (containers > 1) {
54 | add(translateHighlight("worldtools.capture.containers", "%,d".format(containers)))
55 | }
56 | }
57 |
58 | return if (savedElements.isEmpty()) {
59 | translateHighlight("worldtools.capture.nothing_saved_yet", currentLevelName)
60 | } else {
61 | val dimensionsFormatted = dimensions.map {
62 | Text.literal(it).styled { text ->
63 | text.withColor(TextColor.fromRgb(config.render.accentColor))
64 | }
65 | }.joinWithAnd()
66 | Text.translatable("worldtools.capture.saved").copy()
67 | .append(savedElements.joinWithAnd())
68 | .append(Text.translatable("worldtools.capture.in_dimension"))
69 | .append(dimensionsFormatted)
70 | }
71 | }
72 |
73 | fun List.joinWithAnd(): Text {
74 | val and = Text.translatable("worldtools.capture.and")
75 | return when (size) {
76 | 0 -> Text.of("")
77 | 1 -> this[0]
78 | 2 -> this[0].copy().append(and).append(this[1])
79 | else -> dropLast(1).join().append(and).append(last())
80 | }
81 | }
82 |
83 | private fun List.join(): MutableText {
84 | val comma = Text.of(", ")
85 | return foldIndexed(Text.literal("")) { index, acc, text ->
86 | if (index == 0) return@foldIndexed text.copy()
87 | acc.append(comma).append(text)
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/Cacheable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | interface Cacheable {
4 | fun cache()
5 |
6 | fun flush()
7 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/CustomRegionBasedStorage.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap
4 | import net.minecraft.block.entity.BlockEntity
5 | import net.minecraft.nbt.NbtCompound
6 | import net.minecraft.nbt.NbtIo
7 | import net.minecraft.registry.Registries
8 | import net.minecraft.util.Identifier
9 | import net.minecraft.util.PathUtil
10 | import net.minecraft.util.ThrowableDeliverer
11 | import net.minecraft.util.math.BlockPos
12 | import net.minecraft.util.math.ChunkPos
13 | import net.minecraft.world.World
14 | import net.minecraft.world.storage.RegionFile
15 | import net.minecraft.world.storage.StorageKey
16 | import org.waste.of.time.WorldTools.MCA_EXTENSION
17 | import org.waste.of.time.WorldTools.MOD_NAME
18 | import org.waste.of.time.WorldTools.mc
19 | import java.io.DataOutput
20 | import java.io.IOException
21 | import java.nio.file.Path
22 |
23 |
24 | open class CustomRegionBasedStorage internal constructor(
25 | private val directory: Path,
26 | private val dsync: Boolean
27 | ) : AutoCloseable {
28 | private val cachedRegionFiles: Long2ObjectLinkedOpenHashMap = Long2ObjectLinkedOpenHashMap()
29 |
30 | companion object {
31 | // Seems to only be used for MC's profiler
32 | // simpler to just use a default key instead of wiring this all in here
33 | val defaultStorageKey: StorageKey = StorageKey(MOD_NAME, World.OVERWORLD, "chunk")
34 | }
35 |
36 | @Throws(IOException::class)
37 | fun getRegionFile(pos: ChunkPos): RegionFile {
38 | val longPos = ChunkPos.toLong(pos.regionX, pos.regionZ)
39 | cachedRegionFiles.getAndMoveToFirst(longPos)?.let { return it }
40 |
41 | if (cachedRegionFiles.size >= 256) {
42 | cachedRegionFiles.removeLast()?.close()
43 | }
44 |
45 | PathUtil.createDirectories(directory)
46 | val path = directory.resolve("r." + pos.regionX + "." + pos.regionZ + MCA_EXTENSION)
47 | val regionFile = RegionFile(defaultStorageKey, path, directory, dsync)
48 | cachedRegionFiles.putAndMoveToFirst(longPos, regionFile)
49 | return regionFile
50 | }
51 |
52 | @Throws(IOException::class)
53 | fun write(pos: ChunkPos, nbt: NbtCompound?) {
54 | val regionFile = getRegionFile(pos)
55 | if (nbt == null) {
56 | regionFile.delete(pos)
57 | } else {
58 | regionFile.getChunkOutputStream(pos).use { dataOutputStream ->
59 | NbtIo.write(nbt, dataOutputStream as DataOutput)
60 | }
61 | }
62 | }
63 |
64 | private fun getNbtAt(chunkPos: ChunkPos) =
65 | getRegionFile(chunkPos).getChunkInputStream(chunkPos)?.use { dataInputStream ->
66 | NbtIo.readCompound(dataInputStream)
67 | }
68 |
69 | fun getBlockEntities(chunkPos: ChunkPos): List =
70 | getNbtAt(chunkPos)
71 | ?.getList("block_entities", 10)
72 | ?.filterIsInstance()
73 | ?.mapNotNull { compoundTag ->
74 | val blockPos = BlockPos(compoundTag.getInt("x"), compoundTag.getInt("y"), compoundTag.getInt("z"))
75 | val blockStateIdentifier = Identifier.of(compoundTag.getString("id"))
76 | val world = mc.world ?: return@mapNotNull null
77 |
78 | runCatching {
79 | val block = Registries.BLOCK.get(blockStateIdentifier)
80 | Registries.BLOCK_ENTITY_TYPE
81 | .getOptionalValue(blockStateIdentifier)
82 | .orElse(null)
83 | ?.instantiate(blockPos, block.defaultState)?.apply {
84 | read(compoundTag, world.registryManager)
85 | }
86 | }.getOrNull()
87 | } ?: emptyList()
88 |
89 | @Throws(IOException::class)
90 | override fun close() {
91 | val throwableDeliverer = ThrowableDeliverer()
92 |
93 | cachedRegionFiles.values.filterNotNull().forEach { regionFile ->
94 | try {
95 | regionFile.close()
96 | } catch (iOException: IOException) {
97 | throwableDeliverer.add(iOException)
98 | }
99 | }
100 |
101 | throwableDeliverer.deliver()
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/PathTreeNode.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | class PathTreeNode(private val name: String) {
4 | private val children = mutableListOf()
5 |
6 | fun getOrCreateChild(name: String): PathTreeNode {
7 | children.find { it.name == name }?.let { return it }
8 | val newChild = PathTreeNode(name)
9 | children.add(newChild)
10 | return newChild
11 | }
12 |
13 | fun buildTreeString(stringBuilder: StringBuilder, prefix: String, isLast: Boolean) {
14 | stringBuilder.apply {
15 | if (prefix.isNotEmpty()) {
16 | append(prefix)
17 | append(if (isLast) "└─ " else "├─ ")
18 | append(name)
19 | append("\n")
20 | } else {
21 | append(name)
22 | append("\n")
23 | }
24 |
25 | children.forEachIndexed { index, treeNode ->
26 | val isLastChild = index == children.size - 1
27 | val childPrefix = if (isLast) "$prefix " else "$prefix│ "
28 | treeNode.buildTreeString(this, childPrefix, isLastChild)
29 | }
30 | }
31 | }
32 |
33 | companion object {
34 | fun buildTree(paths: List): String {
35 | val root = PathTreeNode("minecraft")
36 | paths.forEach { path ->
37 | var currentNode = root
38 | path.split("/").forEach {
39 | currentNode = currentNode.getOrCreateChild(it)
40 | }
41 | }
42 | val stringBuilder = StringBuilder()
43 | root.buildTreeString(stringBuilder, "", true)
44 | return stringBuilder.toString()
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/RegionBased.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | import net.minecraft.nbt.NbtCompound
4 | import net.minecraft.util.WorldSavePath
5 | import net.minecraft.util.math.ChunkPos
6 | import net.minecraft.world.World
7 | import net.minecraft.world.level.storage.LevelStorage
8 | import org.waste.of.time.WorldTools.LOG
9 |
10 | abstract class RegionBased(
11 | val chunkPos: ChunkPos,
12 | val world: World,
13 | private val suffix: String
14 | ) : Storeable() {
15 | val dimension: String = world.registryKey.value.path
16 |
17 | private val dimensionPath
18 | get() = when (dimension) {
19 | "overworld" -> ""
20 | "the_nether" -> "DIM-1/"
21 | "the_end" -> "DIM1/"
22 | else -> "dimensions/minecraft/$dimension/"
23 | }
24 |
25 | abstract fun compound(): NbtCompound
26 |
27 | abstract fun incrementStats()
28 |
29 | // can be overridden but super should be called after
30 | open fun writeToStorage(
31 | session: LevelStorage.Session,
32 | storage: CustomRegionBasedStorage,
33 | cachedStorages: MutableMap
34 | ) {
35 | try {
36 | storage.write(
37 | chunkPos,
38 | compound()
39 | )
40 | incrementStats()
41 | } catch (e: Exception) {
42 | LOG.error("Failed to store $this", e)
43 | }
44 | }
45 |
46 | override fun store(
47 | session: LevelStorage.Session,
48 | cachedStorages: MutableMap
49 | ) {
50 | if (!shouldStore()) return
51 | val storage = generateStorage(session, cachedStorages)
52 | writeToStorage(session, storage, cachedStorages)
53 | }
54 |
55 | fun generateStorage(
56 | session: LevelStorage.Session,
57 | cachedStorages: MutableMap
58 | ): CustomRegionBasedStorage {
59 | val path = session.getDirectory(WorldSavePath.ROOT)
60 | .resolve(dimensionPath)
61 | .resolve(suffix)
62 | return cachedStorages.getOrPut(path.toString()) {
63 | CustomRegionBasedStorage(path, false)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/StorageFlow.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.channels.BufferOverflow
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.launch
8 | import net.minecraft.util.path.SymlinkValidationException
9 | import org.waste.of.time.WorldTools
10 | import org.waste.of.time.WorldTools.LOG
11 | import org.waste.of.time.WorldTools.mc
12 | import org.waste.of.time.manager.CaptureManager
13 | import org.waste.of.time.manager.MessageManager
14 | import org.waste.of.time.manager.StatisticManager
15 | import org.waste.of.time.storage.cache.HotCache
16 | import org.waste.of.time.storage.serializable.BlockEntityLoadable
17 | import org.waste.of.time.storage.serializable.EndFlow
18 | import java.io.IOException
19 | import java.util.concurrent.CancellationException
20 | import kotlin.time.Duration
21 | import kotlin.time.measureTime
22 |
23 | object StorageFlow {
24 | private const val MAX_BUFFER_SIZE = 10000
25 | var lastStoredTimestamp: Long = 0
26 | var lastStored: Storeable? = null
27 | var lastStoredTimeNeeded: Duration = Duration.ZERO
28 |
29 | private val sharedFlow = MutableSharedFlow(extraBufferCapacity = MAX_BUFFER_SIZE)
30 |
31 | fun emit(storeable: Storeable) {
32 | if (sharedFlow.tryEmit(storeable)) return
33 |
34 | LOG.warn("Buffer overflow: Unable to emit \"${storeable.formattedInfo.string}\" to storage flow")
35 | }
36 |
37 | fun launch(levelName: String) = CoroutineScope(Dispatchers.IO).launch {
38 | StatisticManager.reset()
39 | val cachedStorages = mutableMapOf()
40 |
41 | try {
42 | LOG.info("Started caching")
43 | mc.levelStorage.createSession(levelName).use { openSession ->
44 | sharedFlow.collect { storeable ->
45 | if (!storeable.shouldStore()) {
46 | return@collect
47 | }
48 |
49 | val shouldSaveLastStored: Boolean
50 | val time = measureTime {
51 | shouldSaveLastStored = (storeable as? BlockEntityLoadable)?.load(openSession, cachedStorages) ?: true
52 | storeable.store(openSession, cachedStorages)
53 | }
54 |
55 | if (shouldSaveLastStored) {
56 | lastStored = storeable
57 | lastStoredTimestamp = System.currentTimeMillis()
58 | lastStoredTimeNeeded = time
59 | }
60 |
61 | if (storeable is EndFlow) {
62 | throw StopCollectingException()
63 | }
64 | }
65 | }
66 | } catch (e: StopCollectingException) {
67 | LOG.info("Canceled caching flow")
68 | } catch (e: IOException) {
69 | LOG.error("IOException: Failed to create session for $levelName", e)
70 | MessageManager.sendError("worldtools.log.error.failed_to_create_session", levelName, e.localizedMessage)
71 | } catch (e: SymlinkValidationException) {
72 | LOG.error("SymlinkValidationException: Failed to create session for $levelName", e)
73 | MessageManager.sendError("worldtools.log.error.failed_to_create_session", levelName, e.localizedMessage)
74 | } catch (e: CancellationException) {
75 | LOG.info("Canceled caching thread")
76 | } catch (e: Throwable) {
77 | LOG.error("Unhandled storage flow error", e)
78 | }
79 |
80 | cachedStorages.values.forEach { it.close() }
81 | HotCache.clear()
82 | CaptureManager.capturing = false
83 | LOG.info("Finished caching")
84 | }
85 |
86 | class StopCollectingException : Exception()
87 | }
88 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/Storeable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage
2 |
3 | import net.minecraft.text.MutableText
4 | import net.minecraft.text.Text
5 | import net.minecraft.world.level.storage.LevelStorage.Session
6 | import org.waste.of.time.manager.MessageManager.translateHighlight
7 | import org.waste.of.time.WorldTools.config
8 |
9 | abstract class Storeable {
10 | abstract fun store(
11 | session: Session,
12 | cachedStorages: MutableMap
13 | )
14 |
15 | abstract fun shouldStore(): Boolean
16 |
17 | abstract val verboseInfo: MutableText
18 |
19 | abstract val anonymizedInfo: MutableText
20 |
21 | fun emit() = StorageFlow.emit(this)
22 |
23 | val formattedInfo: Text by lazy {
24 | if (config.advanced.anonymousMode) {
25 | anonymizedInfo
26 | } else {
27 | verboseInfo
28 | }.append(translateHighlight("worldtools.capture.took", StorageFlow.lastStoredTimeNeeded))
29 | }
30 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/cache/DataInjectionHandler.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.cache
2 |
3 | import net.minecraft.block.ChestBlock
4 | import net.minecraft.block.entity.*
5 | import net.minecraft.block.enums.ChestType
6 | import net.minecraft.client.gui.screen.Screen
7 | import net.minecraft.client.gui.screen.ingame.*
8 | import net.minecraft.entity.Entity
9 | import net.minecraft.entity.player.PlayerInventory
10 | import net.minecraft.entity.vehicle.HopperMinecartEntity
11 | import net.minecraft.entity.vehicle.VehicleInventory
12 | import net.minecraft.inventory.EnderChestInventory
13 | import net.minecraft.inventory.SimpleInventory
14 | import org.waste.of.time.WorldTools.mc
15 | import org.waste.of.time.storage.cache.HotCache.markScanned
16 | import org.waste.of.time.storage.cache.HotCache.scannedBlockEntities
17 |
18 | object DataInjectionHandler {
19 | fun onScreenRemoved(screen: Screen) {
20 | HotCache.lastInteractedBlockEntity?.let {
21 | handleBlockEntity(screen, it)
22 | }
23 | HotCache.lastInteractedEntity?.let {
24 | handleEntity(screen, it)
25 | }
26 | }
27 |
28 | private fun handleEntity(screen: Screen, entity: Entity) {
29 | when (screen) {
30 | is GenericContainerScreen -> {
31 | (entity as? VehicleInventory)?.dataToVehicle(screen)
32 | }
33 | is HopperScreen -> {
34 | (entity as? HopperMinecartEntity)?.dataToHopperMinecart(screen)
35 | }
36 | }
37 |
38 | entity.markScanned()
39 | }
40 |
41 | private fun VehicleInventory.dataToVehicle(screen: GenericContainerScreen) {
42 | screen.getContainerSlots().forEach {
43 | setStack(it.index, it.stack)
44 | }
45 | }
46 |
47 | private fun HopperMinecartEntity.dataToHopperMinecart(screen: HopperScreen) {
48 | screen.getContainerSlots().forEach {
49 | setStack(it.index, it.stack)
50 | }
51 | }
52 |
53 | private fun handleBlockEntity(screen: Screen, blockEntity: BlockEntity, ) {
54 | when (screen) {
55 | is GenericContainerScreen -> {
56 | when (blockEntity) {
57 | is ChestBlockEntity -> blockEntity.dataToChest(screen)
58 | is BarrelBlockEntity -> blockEntity.dataToBarrelBlock(screen)
59 | is EnderChestBlockEntity -> dataToEnderChest(screen)
60 | }
61 | }
62 |
63 | is Generic3x3ContainerScreen -> {
64 | (blockEntity as? DispenserBlockEntity)?.dataToDispenserOrDropper(screen)
65 | }
66 |
67 | is AbstractFurnaceScreen<*> -> {
68 | (blockEntity as? AbstractFurnaceBlockEntity)?.dataToFurnace(screen)
69 | }
70 |
71 | is BrewingStandScreen -> {
72 | (blockEntity as? BrewingStandBlockEntity)?.dataToBrewingStand(screen)
73 | }
74 |
75 | is HopperScreen -> {
76 | (blockEntity as? HopperBlockEntity)?.dataToHopper(screen)
77 | }
78 |
79 | is ShulkerBoxScreen -> {
80 | (blockEntity as? ShulkerBoxBlockEntity)?.dataToShulkerBox(screen)
81 | }
82 |
83 | is LecternScreen -> {
84 | (blockEntity as? LecternBlockEntity)?.dataToLectern(screen)
85 | }
86 |
87 | is CrafterScreen -> {
88 | (blockEntity as? CrafterBlockEntity)?.dataToCrafter(screen)
89 | }
90 | }
91 |
92 | // ToDo: Add support for entity containers like chest boat and minecart
93 |
94 | // ToDo: Find out if its possible to get the map state update (currently has no effect)
95 | // screen.getContainerSlots().filter {
96 | // it.stack.item == Items.FILLED_MAP
97 | // }.forEach {
98 | // it.stack.components.get(DataComponentTypes.MAP_ID)?.let { id ->
99 | // HotCache.mapIDs.add(id.id)
100 | // }
101 | // }
102 |
103 | blockEntity.markScanned()
104 | }
105 |
106 | private fun dataToEnderChest(screen: GenericContainerScreen) {
107 | if (mc.isInSingleplayer) return
108 | val inventory = screen.screenHandler.inventory as? SimpleInventory ?: return
109 | if (inventory.size() != 27) return
110 | mc.player?.enderChestInventory = EnderChestInventory().apply {
111 | repeat(inventory.size()) { i ->
112 | setStack(i, inventory.getStack(i))
113 | }
114 | }
115 | }
116 |
117 | private fun AbstractFurnaceBlockEntity.dataToFurnace(screen: AbstractFurnaceScreen<*>) {
118 | screen.getContainerSlots().forEach {
119 | setStack(it.index, it.stack)
120 | }
121 | }
122 |
123 | private fun BarrelBlockEntity.dataToBarrelBlock(screen: GenericContainerScreen) {
124 | screen.getContainerSlots().forEach {
125 | setStack(it.index, it.stack)
126 | }
127 | }
128 |
129 | private fun BrewingStandBlockEntity.dataToBrewingStand(screen: BrewingStandScreen) {
130 | screen.getContainerSlots().forEach {
131 | setStack(it.index, it.stack)
132 | }
133 | }
134 |
135 | private fun ChestBlockEntity.dataToChest(screen: GenericContainerScreen) {
136 | val facing = cachedState[ChestBlock.FACING] ?: return
137 | val chestType = cachedState[ChestBlock.CHEST_TYPE] ?: return
138 | val containerSlots = screen.getContainerSlots()
139 | val inventories = containerSlots.partition { it.index < 27 }
140 |
141 | when (chestType) {
142 | ChestType.SINGLE -> {
143 | containerSlots.forEach {
144 | setStack(it.index, it.stack)
145 | }
146 | }
147 |
148 | ChestType.LEFT -> {
149 | val pos = pos.offset(facing.rotateYClockwise())
150 | val otherChest = world?.getBlockEntity(pos)
151 | if (otherChest !is ChestBlockEntity) return
152 |
153 | inventories.first.forEach {
154 | otherChest.setStack(it.index, it.stack)
155 | }
156 | inventories.second.forEach {
157 | setStack(it.index - 27, it.stack)
158 | }
159 |
160 | scannedBlockEntities[otherChest.pos] = otherChest
161 | }
162 |
163 | ChestType.RIGHT -> {
164 | val pos = pos.offset(facing.rotateYCounterclockwise())
165 | val otherChest = world?.getBlockEntity(pos)
166 | if (otherChest !is ChestBlockEntity) return
167 |
168 | inventories.first.forEach {
169 | setStack(it.index, it.stack)
170 | }
171 | inventories.second.forEach {
172 | otherChest.setStack(it.index - 27, it.stack)
173 | }
174 |
175 | scannedBlockEntities[otherChest.pos] = otherChest
176 | }
177 | }
178 | }
179 |
180 | private fun DispenserBlockEntity.dataToDispenserOrDropper(screen: Generic3x3ContainerScreen) {
181 | screen.getContainerSlots().forEach {
182 | setStack(it.index, it.stack)
183 | }
184 | }
185 |
186 | private fun HopperBlockEntity.dataToHopper(screen: HopperScreen) {
187 | screen.getContainerSlots().forEach {
188 | setStack(it.index, it.stack)
189 | }
190 | }
191 |
192 | private fun ShulkerBoxBlockEntity.dataToShulkerBox(screen: ShulkerBoxScreen) {
193 | screen.getContainerSlots().forEach {
194 | setStack(it.index, it.stack)
195 | }
196 | }
197 |
198 | private fun LecternBlockEntity.dataToLectern(screen: LecternScreen) {
199 | book = screen.screenHandler.bookItem
200 | }
201 |
202 | private fun CrafterBlockEntity.dataToCrafter(screen: CrafterScreen) {
203 | screen.getContainerSlots().forEach {
204 | setStack(it.index, it.stack)
205 | setSlotEnabled(it.index, !isSlotDisabled(it.index))
206 | }
207 | }
208 |
209 | private fun HandledScreen<*>.getContainerSlots() = screenHandler.slots.filter { it.inventory !is PlayerInventory }
210 | }
211 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/cache/EntityCacheable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.cache
2 |
3 | import net.minecraft.entity.Entity
4 | import net.minecraft.entity.EntityType
5 | import net.minecraft.nbt.NbtCompound
6 | import org.waste.of.time.Utils.toByte
7 | import org.waste.of.time.WorldTools.TIMESTAMP_KEY
8 | import org.waste.of.time.WorldTools.config
9 | import org.waste.of.time.storage.Cacheable
10 |
11 | data class EntityCacheable(
12 | val entity: Entity
13 | ) : Cacheable {
14 | fun compound() = NbtCompound().apply {
15 | // saveSelfNbt has a check for RemovalReason.DISCARDED
16 | EntityType.getId(entity.type)?.let { putString(Entity.ID_KEY, it.toString()) }
17 | entity.writeNbt(this)
18 |
19 | if (config.entity.behavior.modifyEntityBehavior) {
20 | putByte("NoAI", config.entity.behavior.noAI.toByte())
21 | putByte("NoGravity", config.entity.behavior.noGravity.toByte())
22 | putByte("Invulnerable", config.entity.behavior.invulnerable.toByte())
23 | putByte("Silent", config.entity.behavior.silent.toByte())
24 | }
25 |
26 | if (config.entity.metadata.captureTimestamp) {
27 | putLong(TIMESTAMP_KEY, System.currentTimeMillis())
28 | }
29 | }
30 |
31 | override fun cache() {
32 | HotCache.entities.computeIfAbsent(entity.chunkPos) { mutableSetOf() }.apply {
33 | // Remove the entity if it already exists to update it
34 | removeIf { it.entity.uuid == entity.uuid }
35 | add(this@EntityCacheable)
36 | }
37 | }
38 |
39 | override fun flush() {
40 | val chunkPos = entity.chunkPos
41 | HotCache.entities[chunkPos]?.let { list ->
42 | list.remove(this)
43 | if (list.isEmpty()) {
44 | HotCache.entities.remove(chunkPos)
45 | }
46 | }
47 | }
48 |
49 | override fun equals(other: Any?): Boolean {
50 | if (other !is EntityCacheable) return super.equals(other)
51 | return entity.uuid == other.entity.uuid
52 | }
53 |
54 | override fun hashCode() = entity.uuid.hashCode()
55 | }
56 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/cache/HotCache.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.cache
2 |
3 | import it.unimi.dsi.fastutil.longs.LongOpenHashSet
4 | import net.minecraft.block.entity.BlockEntity
5 | import net.minecraft.block.entity.LecternBlockEntity
6 | import net.minecraft.block.entity.LockableContainerBlockEntity
7 | import net.minecraft.entity.Entity
8 | import net.minecraft.entity.vehicle.VehicleInventory
9 | import net.minecraft.inventory.EnderChestInventory
10 | import net.minecraft.registry.Registries
11 | import net.minecraft.util.math.BlockPos
12 | import net.minecraft.util.math.ChunkPos
13 | import net.minecraft.world.World
14 | import org.waste.of.time.WorldTools.LOG
15 | import org.waste.of.time.WorldTools.config
16 | import org.waste.of.time.WorldTools.mc
17 | import org.waste.of.time.manager.StatisticManager
18 | import org.waste.of.time.storage.serializable.PlayerStoreable
19 | import org.waste.of.time.storage.serializable.RegionBasedChunk
20 | import org.waste.of.time.storage.serializable.RegionBasedEntities
21 | import java.util.concurrent.ConcurrentHashMap
22 |
23 | /**
24 | * [HotCache] that caches all currently loaded objects in the world that are needed for the world download.
25 | * It will be maintained until the user stops the capture process.
26 | * Then the data will be released into the storage data flow to be serialized in the storage thread.
27 | * This is needed because objects won't be saved to disk until they are unloaded from the world.
28 | */
29 | object HotCache {
30 | val chunks = ConcurrentHashMap()
31 | internal val savedChunks = LongOpenHashSet()
32 | val entities = ConcurrentHashMap>()
33 | val players: ConcurrentHashMap.KeySetView = ConcurrentHashMap.newKeySet()
34 | val scannedBlockEntities = ConcurrentHashMap()
35 | private val scannedEntities: ConcurrentHashMap.KeySetView = ConcurrentHashMap.newKeySet()
36 | val loadedBlockEntities = ConcurrentHashMap()
37 | var lastInteractedBlockEntity: BlockEntity? = null
38 | var lastInteractedEntity: Entity? = null
39 | val unscannedBlockEntities by LazyUpdatingDelegate(100) {
40 | chunks.values
41 | .flatMap { it.chunk.blockEntities.values }
42 | .filter { it.isSupported }
43 | .filterNot { scannedBlockEntities.containsKey(it.pos) }
44 | }
45 | val unscannedEntities by LazyUpdatingDelegate(100) {
46 | entities.values
47 | .flatten()
48 | .filter { it.entity.isSupported }
49 | .filterNot { it.entity in scannedEntities }
50 | }
51 | // map id's of maps that we've seen during the capture
52 | val mapIDs = mutableSetOf()
53 | val BlockEntity.isSupported get() =
54 | this is LockableContainerBlockEntity
55 | || this is LecternBlockEntity
56 | val Entity.isSupported get() = this is VehicleInventory
57 |
58 | fun getEntitySerializableForChunk(chunkPos: ChunkPos, world: World) =
59 | entities[chunkPos]?.let { entities ->
60 | RegionBasedEntities(chunkPos, entities, world)
61 | }
62 |
63 | /**
64 | * Used as a public API for external mods like [XaeroPlus](https://github.com/rfresh2/XaeroPlus), change carefully.
65 | *
66 | * @param chunkX The X coordinate of the chunk.
67 | * @param chunkZ The Z coordinate of the chunk.
68 | * @return True if the chunk is saved, false otherwise.
69 | */
70 | @Suppress("unused")
71 | fun isChunkSaved(chunkX: Int, chunkZ: Int) = savedChunks.contains(ChunkPos.toLong(chunkX, chunkZ))
72 |
73 | fun clear() {
74 | chunks.clear()
75 | savedChunks.clear()
76 | entities.clear()
77 | players.clear()
78 | scannedBlockEntities.clear()
79 | loadedBlockEntities.clear()
80 | mapIDs.clear()
81 |
82 | // failing to reset this could cause users to accidentally save their echest contents on subsequent captures
83 | if (!mc.isInSingleplayer && !config.advanced.keepEnderChestContents) {
84 | mc.player?.enderChestInventory = EnderChestInventory()
85 | }
86 | lastInteractedBlockEntity = null
87 | LOG.info("Cleared hot cache")
88 | }
89 |
90 | fun BlockEntity.markScanned(fromCache: Boolean = false) {
91 | if (fromCache) {
92 | loadedBlockEntities[pos] = this
93 | } else {
94 | scannedBlockEntities[pos] = this
95 | loadedBlockEntities.remove(pos)
96 | }
97 |
98 | world?.registryKey?.value?.path?.let {
99 | StatisticManager.dimensions.add(it)
100 | }
101 | if (config.debug.logSavedContainers) {
102 | LOG.info("Saved block entity: ${Registries.BLOCK_ENTITY_TYPE.getId(type)?.path} at $pos")
103 | }
104 | }
105 |
106 | fun Entity.markScanned() {
107 | scannedEntities.add(this)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/cache/LazyUpdatingDelegate.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.cache
2 |
3 | import kotlin.properties.ReadOnlyProperty
4 | import kotlin.reflect.KProperty
5 |
6 | class LazyUpdatingDelegate(private val timeout: Long, private val block: () -> T) : ReadOnlyProperty {
7 | private var value: T = block()
8 | private var lastUpdateTime: Long = System.currentTimeMillis()
9 |
10 | override fun getValue(thisRef: Any?, property: KProperty<*>): T {
11 | val currentTime = System.currentTimeMillis()
12 | if (currentTime - lastUpdateTime >= timeout) {
13 | value = block()
14 | lastUpdateTime = currentTime
15 | }
16 | return value
17 | }
18 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/AdvancementsStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import com.google.gson.JsonElement
4 | import com.mojang.serialization.JsonOps
5 | import net.minecraft.advancement.PlayerAdvancementTracker
6 | import net.minecraft.datafixer.DataFixTypes
7 | import net.minecraft.text.MutableText
8 | import net.minecraft.util.PathUtil
9 | import net.minecraft.util.WorldSavePath
10 | import net.minecraft.world.level.storage.LevelStorage
11 | import org.waste.of.time.WorldTools.CURRENT_VERSION
12 | import org.waste.of.time.WorldTools.GSON
13 | import org.waste.of.time.WorldTools.LOG
14 | import org.waste.of.time.WorldTools.config
15 | import org.waste.of.time.WorldTools.mc
16 | import org.waste.of.time.manager.MessageManager.translateHighlight
17 | import org.waste.of.time.storage.CustomRegionBasedStorage
18 | import org.waste.of.time.storage.Storeable
19 | import java.nio.charset.StandardCharsets
20 | import java.nio.file.Files
21 |
22 | class AdvancementsStoreable : Storeable() {
23 | override fun shouldStore() = config.general.capture.advancements
24 |
25 | override val verboseInfo: MutableText
26 | get() = translateHighlight(
27 | "worldtools.capture.saved.advancements",
28 | mc.player?.name ?: "Unknown"
29 | )
30 |
31 | override val anonymizedInfo: MutableText
32 | get() = verboseInfo
33 |
34 | private val progressMapCodec =
35 | DataFixTypes.ADVANCEMENTS.createDataFixingCodec(
36 | PlayerAdvancementTracker.ProgressMap.CODEC, mc.dataFixer, CURRENT_VERSION
37 | )
38 |
39 | override fun store(
40 | session: LevelStorage.Session,
41 | cachedStorages: MutableMap
42 | ) {
43 | val uuid = mc.player?.uuid ?: return
44 | val progress = mc.player
45 | ?.networkHandler
46 | ?.advancementHandler
47 | ?.advancementProgresses ?: return
48 | val progressMap = progress.entries
49 | .filter { it.value.isAnyObtained }
50 | .associate {
51 | it.key.id to it.value
52 | }
53 | val jsonElement =
54 | progressMapCodec.encodeStart(
55 | JsonOps.INSTANCE,
56 | PlayerAdvancementTracker.ProgressMap(progressMap)
57 | ).getOrThrow() as JsonElement
58 |
59 |
60 | val advancements = session.getDirectory(WorldSavePath.ADVANCEMENTS)
61 | PathUtil.createDirectories(advancements)
62 | Files.newBufferedWriter(
63 | advancements.resolve("$uuid.json"),
64 | StandardCharsets.UTF_8
65 | ).use { writer ->
66 | GSON.toJson(jsonElement, writer)
67 | }
68 |
69 | LOG.info("Saved ${progressMap.size} advancements.")
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/BlockEntityLoadable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.block.entity.BlockEntity
4 | import net.minecraft.block.entity.LecternBlockEntity
5 | import net.minecraft.block.entity.LockableContainerBlockEntity
6 | import net.minecraft.world.chunk.WorldChunk
7 | import net.minecraft.world.level.storage.LevelStorage
8 | import org.waste.of.time.WorldTools.config
9 | import org.waste.of.time.manager.MessageManager.translateHighlight
10 | import org.waste.of.time.storage.CustomRegionBasedStorage
11 | import org.waste.of.time.storage.cache.HotCache
12 | import org.waste.of.time.storage.cache.HotCache.isSupported
13 | import org.waste.of.time.storage.cache.HotCache.markScanned
14 |
15 | class BlockEntityLoadable(
16 | chunk: WorldChunk
17 | ) : RegionBasedChunk(chunk) {
18 | private var migrated = false
19 | override fun shouldStore() =
20 | config.general.reloadBlockEntities && chunk.blockEntities.isNotEmpty()
21 |
22 | override val verboseInfo = translateHighlight(
23 | "worldtools.capture.loaded.block_entities",
24 | chunk.pos,
25 | chunk.world.registryKey.value.path
26 | )
27 |
28 | override val anonymizedInfo = translateHighlight(
29 | "worldtools.capture.loaded.block_entities.anonymized",
30 | chunk.world.registryKey.value.path
31 | )
32 |
33 | fun load(
34 | session: LevelStorage.Session,
35 | cachedStorages: MutableMap
36 | ): Boolean {
37 | generateStorage(session, cachedStorages)
38 | .getBlockEntities(chunkPos)
39 | .filter { it.isSupported }
40 | .forEach { existing ->
41 | HotCache.chunks[chunkPos]
42 | ?.cachedBlockEntities
43 | ?.get(existing.pos)
44 | ?.let { blockEntity ->
45 | when (blockEntity) {
46 | is LockableContainerBlockEntity -> blockEntity.migrateData(existing)
47 | is LecternBlockEntity -> blockEntity.migrateData(existing)
48 | }
49 | }
50 | }
51 | return migrated
52 | }
53 |
54 | private fun LockableContainerBlockEntity.migrateData(existing: BlockEntity) {
55 | if (existing !is LockableContainerBlockEntity) return
56 | if (!isEmpty) return
57 | heldStacks = existing.heldStacks
58 | markScanned(true)
59 | migrated = true
60 | }
61 |
62 | private fun LecternBlockEntity.migrateData(existing: BlockEntity) {
63 | if (existing !is LecternBlockEntity) return
64 | if (!book.isEmpty) return
65 | book = existing.book
66 | markScanned(true)
67 | migrated = true
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/CompressLevelStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.text.MutableText
4 | import net.minecraft.util.WorldSavePath
5 | import net.minecraft.world.level.storage.LevelStorage
6 | import org.waste.of.time.Utils.toReadableByteCount
7 | import org.waste.of.time.WorldTools.LOG
8 | import org.waste.of.time.WorldTools.config
9 | import org.waste.of.time.WorldTools.mc
10 | import org.waste.of.time.manager.BarManager
11 | import org.waste.of.time.manager.CaptureManager.currentLevelName
12 | import org.waste.of.time.manager.MessageManager
13 | import org.waste.of.time.manager.MessageManager.translateHighlight
14 | import org.waste.of.time.storage.CustomRegionBasedStorage
15 | import org.waste.of.time.storage.StorageFlow
16 | import org.waste.of.time.storage.Storeable
17 | import java.io.IOException
18 | import java.nio.file.*
19 | import java.nio.file.attribute.BasicFileAttributes
20 | import java.util.zip.ZipEntry
21 | import java.util.zip.ZipOutputStream
22 | import kotlin.io.path.name
23 |
24 | class CompressLevelStoreable : Storeable() {
25 | private val zipName: String get() = "$currentLevelName-${System.currentTimeMillis()}.zip"
26 |
27 | override fun shouldStore() = config.general.compressLevel
28 |
29 | override val verboseInfo: MutableText
30 | get() = translateHighlight("worldtools.capture.saved.compressed", zipName)
31 |
32 | override val anonymizedInfo: MutableText
33 | get() = verboseInfo
34 |
35 | override fun store(
36 | session: LevelStorage.Session,
37 | cachedStorages: MutableMap
38 | ) {
39 | val rootPath = session.getDirectory(WorldSavePath.ROOT)
40 | val zipPath = mc.levelStorage.savesDirectory.resolve(zipName)
41 | LOG.info("Zipping $rootPath to $zipPath")
42 |
43 | val totalSize = Files.walk(rootPath).filter { Files.isRegularFile(it) }.mapToLong { Files.size(it) }.sum()
44 |
45 | try {
46 | var totalZippedSize = 0L
47 |
48 | Files.newOutputStream(zipPath).use { outStream ->
49 | ZipOutputStream(outStream).use { zipOut ->
50 | Files.walkFileTree(rootPath, object : SimpleFileVisitor() {
51 | override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
52 | zipFile(file, rootPath, zipOut)
53 |
54 | totalZippedSize += Files.size(file)
55 | val progress = totalZippedSize.toDouble() / totalSize
56 | StorageFlow.lastStoredTimestamp = System.currentTimeMillis()
57 | BarManager.progressBar.percent = progress.toFloat()
58 | if (config.debug.logZippingProgress) {
59 | LOG.info("${"%.2f".format(progress * 100)}% (${totalZippedSize.toReadableByteCount()}/${totalSize.toReadableByteCount()}) Zipping file ${file.name} with size ${Files.size(file).toReadableByteCount()}")
60 | }
61 | return FileVisitResult.CONTINUE
62 | }
63 |
64 | override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
65 | MessageManager.sendError("worldtools.log.error.failed_to_visit_file", file, exc.localizedMessage)
66 | return FileVisitResult.CONTINUE
67 | }
68 | })
69 | }
70 | }
71 | LOG.info("Finished zipping $rootPath with size ${totalZippedSize.toReadableByteCount()} to ${zipPath.toAbsolutePath()} with size ${Files.size(zipPath).toReadableByteCount()}")
72 | } catch (e: IOException) {
73 | MessageManager.sendError("worldtools.log.error.failed_to_zip", rootPath, e.localizedMessage)
74 | }
75 | }
76 |
77 | @Throws(IOException::class)
78 | private fun zipFile(fileToZip: Path, rootPath: Path, zipOut: ZipOutputStream) {
79 | if (fileToZip.fileName.toString().contains("session.lock")) return
80 | val entryName = rootPath.relativize(fileToZip).toString().replace('\\', '/')
81 | when {
82 | Files.isHidden(fileToZip) -> return
83 | Files.isDirectory(fileToZip) -> {
84 | zipOut.putNextEntry(ZipEntry("$entryName/"))
85 | zipOut.closeEntry()
86 | }
87 | else -> {
88 | Files.newInputStream(fileToZip).use { inputStream ->
89 | zipOut.putNextEntry(ZipEntry(entryName))
90 | inputStream.copyTo(zipOut)
91 | zipOut.closeEntry()
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/EndFlow.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.text.ClickEvent
4 | import net.minecraft.text.MutableText
5 | import net.minecraft.text.Text
6 | import net.minecraft.util.WorldSavePath
7 | import net.minecraft.world.level.storage.LevelStorage
8 | import org.waste.of.time.manager.CaptureManager.currentLevelName
9 | import org.waste.of.time.manager.MessageManager.infoToast
10 | import org.waste.of.time.manager.MessageManager.sendInfo
11 | import org.waste.of.time.manager.MessageManager.translateHighlight
12 | import org.waste.of.time.manager.StatisticManager
13 | import org.waste.of.time.storage.CustomRegionBasedStorage
14 | import org.waste.of.time.storage.Storeable
15 |
16 | class EndFlow : Storeable() {
17 | override fun shouldStore() = true
18 |
19 | override val verboseInfo: MutableText
20 | get() = translateHighlight(
21 | "worldtools.capture.saved.end_flow",
22 | currentLevelName
23 | )
24 |
25 | override val anonymizedInfo: MutableText
26 | get() = verboseInfo
27 |
28 | override fun store(
29 | session: LevelStorage.Session,
30 | cachedStorages: MutableMap
31 | ) {
32 | StatisticManager.infoMessage.apply {
33 | infoToast()
34 |
35 | val directory = Text.translatable("worldtools.capture.to_directory")
36 | val clickToOpen = translateHighlight(
37 | "worldtools.capture.click_to_open",
38 | currentLevelName
39 | ).copy().styled {
40 | it.withClickEvent(
41 | ClickEvent(
42 | ClickEvent.Action.OPEN_FILE,
43 | session.getDirectory(WorldSavePath.ROOT).toFile().path
44 | )
45 | )
46 | }
47 |
48 | copy().append(directory).append(clickToOpen).sendInfo()
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/LevelDataStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.SharedConstants
4 | import net.minecraft.nbt.*
5 | import net.minecraft.text.MutableText
6 | import net.minecraft.util.Util
7 | import net.minecraft.util.WorldSavePath
8 | import net.minecraft.world.GameRules
9 | import net.minecraft.world.level.storage.LevelStorage.Session
10 | import org.waste.of.time.Utils.toByte
11 | import org.waste.of.time.WorldTools.DAT_EXTENSION
12 | import org.waste.of.time.WorldTools.LOG
13 | import org.waste.of.time.WorldTools.config
14 | import org.waste.of.time.WorldTools.mc
15 | import org.waste.of.time.config.WorldToolsConfig.World.WorldGenerator.GeneratorType
16 | import org.waste.of.time.manager.CaptureManager
17 | import org.waste.of.time.manager.CaptureManager.currentLevelName
18 | import org.waste.of.time.manager.MessageManager
19 | import org.waste.of.time.manager.MessageManager.translateHighlight
20 | import org.waste.of.time.storage.CustomRegionBasedStorage
21 | import org.waste.of.time.storage.Storeable
22 | import java.io.File
23 | import java.io.IOException
24 |
25 | class LevelDataStoreable : Storeable() {
26 | override fun shouldStore() = config.general.capture.levelData
27 |
28 | override val verboseInfo: MutableText
29 | get() = translateHighlight(
30 | "worldtools.capture.saved.levelData",
31 | currentLevelName,
32 | "level${DAT_EXTENSION}"
33 | )
34 |
35 | override val anonymizedInfo: MutableText
36 | get() = verboseInfo
37 |
38 | /**
39 | * See [net.minecraft.world.level.storage.LevelStorage.Session.backupLevelDataFile]
40 | */
41 | override fun store(
42 | session: Session,
43 | cachedStorages: MutableMap
44 | ) {
45 | val resultingFile = session.getDirectory(WorldSavePath.ROOT).toFile()
46 | val dataNbt = serializeLevelData()
47 | // if we save an empty level.dat, clients will crash when opening the SP worlds screen
48 | if (dataNbt.isEmpty) throw RuntimeException("Failed to serialize level data")
49 | val levelNbt = NbtCompound().apply {
50 | put("Data", dataNbt)
51 | }
52 |
53 | try {
54 | val newFile = File.createTempFile("level", DAT_EXTENSION, resultingFile).toPath()
55 | NbtIo.writeCompressed(levelNbt, newFile)
56 | val backup = session.getDirectory(WorldSavePath.LEVEL_DAT_OLD)
57 | val current = session.getDirectory(WorldSavePath.LEVEL_DAT)
58 | Util.backupAndReplace(current, newFile, backup)
59 | LOG.info("Saved level data.")
60 | } catch (exception: IOException) {
61 | MessageManager.sendError(
62 | "worldtools.log.error.failed_to_save_level",
63 | resultingFile.path,
64 | exception.localizedMessage
65 | )
66 | }
67 | }
68 |
69 | /**
70 | * See [net.minecraft.world.level.LevelProperties.updateProperties]
71 | */
72 | private fun serializeLevelData() = NbtCompound().apply {
73 | val player = CaptureManager.lastPlayer ?: mc.player ?: return@apply
74 |
75 | mc.networkHandler?.brand?.let {
76 | put("ServerBrands", NbtList().apply {
77 | add(NbtString.of(it))
78 | })
79 | }
80 |
81 | putBoolean("WasModded", false)
82 |
83 | // skip removed features
84 |
85 | put("Version", NbtCompound().apply {
86 | putString("Name", SharedConstants.getGameVersion().name)
87 | putInt("Id", SharedConstants.getGameVersion().saveVersion.id)
88 | putBoolean("Snapshot", !SharedConstants.getGameVersion().isStable)
89 | putString("Series", SharedConstants.getGameVersion().saveVersion.series)
90 | })
91 |
92 | NbtHelper.putDataVersion(this)
93 |
94 | put("WorldGenSettings", generatorMockNbt())
95 | mc.networkHandler?.listedPlayerListEntries?.find {
96 | it.profile.id == player.uuid
97 | }?.let {
98 | putInt("GameType", it.gameMode.id)
99 | } ?: putInt("GameType", player.server?.defaultGameMode?.id ?: 0)
100 |
101 | putInt("SpawnX", player.world.levelProperties.spawnPos.x)
102 | putInt("SpawnY", player.world.levelProperties.spawnPos.y)
103 | putInt("SpawnZ", player.world.levelProperties.spawnPos.z)
104 | putFloat("SpawnAngle", player.world.levelProperties.spawnAngle)
105 | putLong("Time", player.world.time)
106 | putLong("DayTime", player.world.timeOfDay)
107 | putLong("LastPlayed", System.currentTimeMillis())
108 | putString("LevelName", currentLevelName)
109 | putInt("version", 19133)
110 | putInt("clearWeatherTime", 0) // not sure
111 | putInt("rainTime", 0) // not sure
112 | putBoolean("raining", player.world.isRaining)
113 | putBoolean("thundering", player.world.isThundering)
114 | putBoolean("hardcore", player.server?.isHardcore ?: false)
115 | putInt("thunderTime", 0) // not sure
116 | putBoolean("allowCommands", true) // not sure
117 | putBoolean("initialized", true) // not sure
118 |
119 | player.world.worldBorder.write().writeNbt(this)
120 |
121 | putByte("Difficulty", player.world.levelProperties.difficulty.id.toByte())
122 | putBoolean("DifficultyLocked", false) // not sure
123 |
124 | // ToDo: Seems that the client side game rules were removed. Now only works for single player :/
125 | val rules = player.world?.server?.gameRules?.genGameRules() ?: NbtCompound()
126 | put("GameRules", rules)
127 | put("Player", NbtCompound().apply {
128 | player.writeNbt(this)
129 | remove("LastDeathLocation") // can contain sensitive information
130 | putString("Dimension", "minecraft:${player.world.registryKey.value.path}")
131 | })
132 |
133 | put("DragonFight", NbtCompound()) // not sure
134 | put("CustomBossEvents", NbtCompound()) // not sure
135 | put("ScheduledEvents", NbtList()) // not sure
136 | putInt("WanderingTraderSpawnDelay", 0) // not sure
137 | putInt("WanderingTraderSpawnChance", 0) // not sure
138 |
139 | // skip wandering trader id
140 | }
141 |
142 | private fun GameRules.genGameRules() = toNbt().apply {
143 | val setting = config.world.gameRules
144 | if (!setting.modifyGameRules) return@apply
145 |
146 | putString(GameRules.DO_WARDEN_SPAWNING.name, setting.doWardenSpawning.toString())
147 | putString(GameRules.DO_FIRE_TICK.name, setting.doFireTick.toString())
148 | putString(GameRules.DO_VINES_SPREAD.name, setting.doVinesSpread.toString())
149 | putString(GameRules.DO_MOB_SPAWNING.name, setting.doMobSpawning.toString())
150 | putString(GameRules.DO_DAYLIGHT_CYCLE.name, setting.doDaylightCycle.toString())
151 | putString(GameRules.KEEP_INVENTORY.name, setting.keepInventory.toString())
152 | putString(GameRules.DO_MOB_GRIEFING.name, setting.doMobGriefing.toString())
153 | putString(GameRules.DO_TRADER_SPAWNING.name, setting.doTraderSpawning.toString())
154 | putString(GameRules.DO_PATROL_SPAWNING.name, setting.doPatrolSpawning.toString())
155 | putString(GameRules.DO_WEATHER_CYCLE.name, setting.doWeatherCycle.toString())
156 | }
157 |
158 | private fun generatorMockNbt() = NbtCompound().apply {
159 | putByte("bonus_chest", config.world.worldGenerator.bonusChest.toByte())
160 | putLong("seed", config.world.worldGenerator.seed)
161 | putByte("generate_features", config.world.worldGenerator.generateFeatures.toByte())
162 |
163 | put("dimensions", NbtCompound().apply {
164 | CaptureManager.lastWorldKeys.forEach { key ->
165 | put("minecraft:${key.value.path}", NbtCompound().apply {
166 | put("generator", generateGenerator(key.value.path))
167 |
168 | when (key.value.path) {
169 | "the_nether" -> {
170 | putString("type", "minecraft:the_nether")
171 | }
172 | "the_end" -> {
173 | putString("type", "minecraft:the_end")
174 | }
175 | else -> {
176 | putString("type", "minecraft:overworld")
177 | }
178 | }
179 | })
180 | }
181 | })
182 | }
183 |
184 | private fun generateGenerator(path: String) = NbtCompound().apply {
185 | when (config.world.worldGenerator.type) {
186 | GeneratorType.VOID -> voidGenerator()
187 | GeneratorType.DEFAULT -> defaultGenerator(path)
188 | GeneratorType.FLAT -> flatGenerator()
189 | }
190 | }
191 |
192 | private fun NbtCompound.voidGenerator() {
193 | put("settings", NbtCompound().apply {
194 | putByte("features", 1)
195 | putString("biome", "minecraft:the_void")
196 | put("layers", NbtList().apply {
197 | add(NbtCompound().apply {
198 | putString("block", "minecraft:air")
199 | putInt("height", 1)
200 | })
201 | })
202 | put("structure_overrides", NbtList())
203 | putByte("lakes", 0)
204 | })
205 | putString("type", "minecraft:flat")
206 | }
207 |
208 | private fun NbtCompound.defaultGenerator(path: String) {
209 | when (path) {
210 | "the_nether" -> {
211 | put("biome_source", NbtCompound().apply {
212 | putString("preset", "minecraft:nether")
213 | putString("type", "minecraft:multi_noise")
214 | })
215 | putString("settings", "minecraft:nether")
216 | putString("type", "minecraft:noise")
217 | }
218 | "the_end" -> {
219 | put("biome_source", NbtCompound().apply {
220 | putString("type", "minecraft:the_end")
221 | })
222 | putString("settings", "minecraft:end")
223 | putString("type", "minecraft:noise")
224 | }
225 | else -> {
226 | put("biome_source", NbtCompound().apply {
227 | putString("preset", "minecraft:overworld")
228 | putString("type", "minecraft:multi_noise")
229 | })
230 | putString("settings", "minecraft:overworld")
231 | putString("type", "minecraft:noise")
232 | }
233 | }
234 | }
235 |
236 | private fun NbtCompound.flatGenerator() {
237 | put("settings", NbtCompound().apply {
238 | putString("biome", "minecraft:plains")
239 | putByte("features", 0)
240 | putByte("lakes", 0)
241 | put("layers", NbtList().apply {
242 | add(NbtCompound().apply {
243 | putString("block", "minecraft:bedrock")
244 | putInt("height", 1)
245 | })
246 | add(NbtCompound().apply {
247 | putString("block", "minecraft:dirt")
248 | putInt("height", 2)
249 | })
250 | add(NbtCompound().apply {
251 | putString("block", "minecraft:grass_block")
252 | putInt("height", 1)
253 | })
254 | })
255 | put("structure_overrides", NbtList().apply {
256 | add(NbtString.of("minecraft:strongholds"))
257 | add(NbtString.of("minecraft:villages"))
258 | })
259 | })
260 | putString("type", "minecraft:flat")
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/MapDataStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.nbt.NbtCompound
4 | import net.minecraft.nbt.NbtHelper
5 | import net.minecraft.nbt.NbtIo
6 | import net.minecraft.text.MutableText
7 | import net.minecraft.util.WorldSavePath
8 | import net.minecraft.world.level.storage.LevelStorage
9 | import org.waste.of.time.WorldTools
10 | import org.waste.of.time.WorldTools.LOG
11 | import org.waste.of.time.WorldTools.config
12 | import org.waste.of.time.WorldTools.mc
13 | import org.waste.of.time.manager.CaptureManager
14 | import org.waste.of.time.manager.MessageManager
15 | import org.waste.of.time.storage.CustomRegionBasedStorage
16 | import org.waste.of.time.storage.Storeable
17 | import org.waste.of.time.storage.cache.HotCache
18 | import kotlin.io.path.exists
19 |
20 | class MapDataStoreable : Storeable() {
21 | override fun shouldStore() = config.general.capture.maps
22 | override val verboseInfo: MutableText
23 | get() = MessageManager.translateHighlight(
24 | "worldtools.capture.saved.mapData",
25 | CaptureManager.currentLevelName
26 | )
27 | override val anonymizedInfo: MutableText
28 | get() = verboseInfo
29 |
30 | override fun store(
31 | session: LevelStorage.Session,
32 | cachedStorages: MutableMap
33 | ) {
34 | // this map doesn't seem to be cleared until the world closes
35 | val dataDirectory = session.getDirectory(WorldSavePath.ROOT).resolve("data")
36 | if (!dataDirectory.toFile().exists()) {
37 | dataDirectory.toFile().mkdirs()
38 | }
39 |
40 | mc.world?.let { world ->
41 | world.mapStates?.filter { (component, _) ->
42 | HotCache.mapIDs.contains(component.id)
43 | }?.forEach { (component, mapState) ->
44 | val id = component.id
45 | NbtCompound().apply {
46 | put("data", mapState.writeNbt(NbtCompound(), world.registryManager))
47 | NbtHelper.putDataVersion(this)
48 | val mapFile = dataDirectory.resolve("map_$id${WorldTools.DAT_EXTENSION}")
49 | if (!mapFile.exists()) {
50 | mapFile.toFile().createNewFile()
51 | }
52 | NbtIo.writeCompressed(this, mapFile)
53 | if (config.debug.logSavedMaps) {
54 | LOG.info("Map data saved: $id")
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/MetadataStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.client.network.PlayerListEntry
4 | import net.minecraft.text.MutableText
5 | import net.minecraft.util.PathUtil
6 | import net.minecraft.util.WorldSavePath
7 | import net.minecraft.world.level.storage.LevelStorage.Session
8 | import org.waste.of.time.Utils
9 | import org.waste.of.time.WorldTools.CREDIT_MESSAGE_MD
10 | import org.waste.of.time.WorldTools.LOG
11 | import org.waste.of.time.WorldTools.MOD_NAME
12 | import org.waste.of.time.WorldTools.config
13 | import org.waste.of.time.WorldTools.mc
14 | import org.waste.of.time.manager.BarManager
15 | import org.waste.of.time.manager.CaptureManager.currentLevelName
16 | import org.waste.of.time.manager.CaptureManager.levelName
17 | import org.waste.of.time.manager.MessageManager.translateHighlight
18 | import org.waste.of.time.storage.CustomRegionBasedStorage
19 | import org.waste.of.time.storage.PathTreeNode
20 | import org.waste.of.time.storage.StorageFlow
21 | import org.waste.of.time.storage.Storeable
22 | import java.net.InetSocketAddress
23 | import java.nio.file.Path
24 | import kotlin.io.path.writeBytes
25 |
26 | class MetadataStoreable : Storeable() {
27 | override fun shouldStore() = config.general.capture.metadata
28 |
29 | override val verboseInfo: MutableText
30 | get() = translateHighlight(
31 | "worldtools.capture.saved.metadata",
32 | currentLevelName
33 | )
34 |
35 | override val anonymizedInfo: MutableText
36 | get() = verboseInfo
37 |
38 | override fun store(session: Session, cachedStorages: MutableMap) {
39 | session.writeIconFile()
40 |
41 | session.getDirectory(WorldSavePath.ROOT).resolve(MOD_NAME).apply {
42 | PathUtil.createDirectories(this)
43 |
44 | writePlayerEntryList()
45 | writeDimensionTree()
46 | writeMetadata()
47 | }
48 | }
49 |
50 | private fun Path.writeMetadata() {
51 | resolve("Capture Metadata.md")
52 | .toFile()
53 | .writeText(createMetadata())
54 |
55 | LOG.info("Saved capture metadata.")
56 | }
57 |
58 | private fun Path.writePlayerEntryList() {
59 | if (mc.isInSingleplayer) return
60 |
61 | mc.networkHandler?.playerList?.let { playerList ->
62 | if (playerList.isEmpty()) return@let
63 | resolve("Player Entry List.csv").toFile()
64 | .writeText(createPlayerEntryList(playerList.toList()))
65 | LOG.info("Saved ${playerList.size} player entry list entries.")
66 | }
67 | }
68 |
69 | private fun Path.writeDimensionTree() {
70 | mc.networkHandler?.worldKeys?.let { keys ->
71 | if (keys.isEmpty()) return@let
72 | resolve("Dimension Tree.txt").toFile()
73 | .writeText(PathTreeNode.buildTree(keys.map { it.value.path }))
74 | LOG.info("Saved ${keys.size} dimensions in tree.")
75 | }
76 | }
77 |
78 | private fun Session.writeIconFile() {
79 | mc.networkHandler?.serverInfo?.favicon?.let { favicon ->
80 | iconFile.ifPresent {
81 | it.writeBytes(favicon)
82 | }
83 | } ?: mc.server?.iconFile?.ifPresent { spIconPath ->
84 | iconFile.ifPresent {
85 | it.writeBytes(spIconPath.toFile().readBytes())
86 | }
87 | }
88 | LOG.info("Saved favicon.")
89 | }
90 |
91 | private fun createMetadata() = StringBuilder().apply {
92 | if (currentLevelName != levelName) {
93 | appendLine("# $currentLevelName ($levelName) World Save - Snapshot Details")
94 | } else {
95 | appendLine("# $currentLevelName World Save - Snapshot Details")
96 | }
97 |
98 | if (mc.isInSingleplayer) {
99 | appendLine("")
100 | } else {
101 | appendLine("")
102 | }
103 |
104 | appendLine()
105 | appendLine("- **Time**: `${Utils.getTime()}` (Timestamp: `${System.currentTimeMillis()}`)")
106 | appendLine("- **Captured By**: `${mc.player?.name?.string}`")
107 |
108 | appendLine()
109 |
110 | mc.networkHandler?.serverInfo?.let { info ->
111 | appendLine("## Server")
112 | if (info.name != "Minecraft Server") {
113 | appendLine("- **List Entry Name**: `${info.name}`")
114 | }
115 | appendLine("- **IP**: `${info.address}`")
116 | if (info.playerCountLabel.string.isNotBlank()) {
117 | appendLine("- **Capacity**: `${info.playerCountLabel.string}`")
118 | }
119 | mc.networkHandler?.let {
120 | appendLine("- **Brand**: `${it.brand}`")
121 | }
122 | appendLine("- **MOTD**: `${info.label.string.split("\n").joinToString(" ")}`")
123 | appendLine("- **Version**: `${info.version.string}`")
124 | appendLine("- **Protocol Version**: `${info.protocolVersion}`")
125 | appendLine("- **Server Type**: `${info.serverType}`")
126 |
127 | info.players?.sample?.let l@ { sample ->
128 | if (sample.isEmpty()) return@l
129 | appendLine("- **Short Label**: `${sample.joinToString { it.name }}`")
130 | }
131 | info.playerListSummary?.let l@ {
132 | if (it.isEmpty()) return@l
133 | appendLine("- **Full Label**: `${it.joinToString(" ") { str -> str.string }}`")
134 | }
135 |
136 | appendLine()
137 | appendLine("## Connection")
138 | (mc.networkHandler?.connection?.address as? InetSocketAddress)?.let {
139 | appendLine("- **Host Name**: `${it.address.canonicalHostName}`")
140 | appendLine("- **Port**: `${it.port}`")
141 | }
142 | } ?: run {
143 | appendLine("## Singleplayer Capture")
144 | appendLine("- **Source World Name**: `${mc.server?.name}`")
145 | appendLine("- **Version**: `${mc.server?.version}`")
146 | }
147 |
148 | mc.networkHandler?.sessionId?.let { id ->
149 | appendLine("- **Session ID**: `$id`")
150 | }
151 |
152 | appendLine()
153 | appendLine(CREDIT_MESSAGE_MD)
154 | }.toString()
155 |
156 | private fun createPlayerEntryList(listEntries: List) = StringBuilder().apply {
157 | appendLine("Name, ID, Game Mode, Latency, Scoreboard Team, Model Type, Session ID, Public Key")
158 |
159 | listEntries.forEachIndexed { i, entry ->
160 | StorageFlow.lastStoredTimestamp = System.currentTimeMillis()
161 | BarManager.progressBar.percent = i.toFloat() / listEntries.size
162 | serializePlayerListEntry(entry)
163 | }
164 | }.toString()
165 |
166 | private fun StringBuilder.serializePlayerListEntry(entry: PlayerListEntry) {
167 | append("${entry.profile.name}, ")
168 | append("${entry.profile.id}, ")
169 | append("${entry.gameMode.name}, ")
170 | append("${entry.latency}, ")
171 | append("${entry.scoreboardTeam?.name}, ")
172 | appendLine(entry.skinTextures.model)
173 | entry.session?.let {
174 | append("${it.sessionId}, ")
175 | append("${it.publicKeyData?.data}, ")
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/PlayerStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.entity.player.PlayerEntity
4 | import net.minecraft.nbt.NbtCompound
5 | import net.minecraft.nbt.NbtIo
6 | import net.minecraft.text.MutableText
7 | import net.minecraft.util.Util
8 | import net.minecraft.util.WorldSavePath
9 | import net.minecraft.world.level.storage.LevelStorage.Session
10 | import org.waste.of.time.Utils.asString
11 | import org.waste.of.time.WorldTools
12 | import org.waste.of.time.WorldTools.config
13 | import org.waste.of.time.manager.MessageManager.translateHighlight
14 | import org.waste.of.time.manager.StatisticManager
15 | import org.waste.of.time.storage.Cacheable
16 | import org.waste.of.time.storage.CustomRegionBasedStorage
17 | import org.waste.of.time.storage.Storeable
18 | import org.waste.of.time.storage.cache.HotCache
19 | import java.io.File
20 | import java.nio.file.Files
21 | import java.nio.file.Path
22 |
23 | data class PlayerStoreable(
24 | val player: PlayerEntity
25 | ) : Cacheable, Storeable() {
26 | override fun shouldStore() = config.general.capture.players
27 |
28 | override val verboseInfo: MutableText
29 | get() = translateHighlight(
30 | "worldtools.capture.saved.player",
31 | player.name,
32 | player.pos.asString(),
33 | player.world.registryKey.value.path
34 | )
35 |
36 | override val anonymizedInfo: MutableText
37 | get() = translateHighlight(
38 | "worldtools.capture.saved.player.anonymized",
39 | player.name,
40 | player.world.registryKey.value.path
41 | )
42 |
43 | override fun cache() {
44 | HotCache.players.add(this)
45 | }
46 |
47 | override fun flush() {
48 | HotCache.players.remove(this)
49 | }
50 |
51 | override fun store(session: Session, cachedStorages: MutableMap) {
52 | savePlayerData(player, session)
53 | session.createSaveHandler()
54 | StatisticManager.players++
55 | StatisticManager.dimensions.add(player.world.registryKey.value.path)
56 | }
57 |
58 | private fun savePlayerData(player: PlayerEntity, session: Session) {
59 | try {
60 | val playerDataDir = session.getDirectory(WorldSavePath.PLAYERDATA).toFile()
61 | playerDataDir.mkdirs()
62 |
63 | val newPlayerFile = File.createTempFile(player.uuidAsString + "-", ".dat", playerDataDir).toPath()
64 | NbtIo.writeCompressed(player.writeNbt(NbtCompound()).apply {
65 | if (config.entity.censor.lastDeathLocation) {
66 | remove("LastDeathLocation")
67 | }
68 | }, newPlayerFile)
69 | val currentFile = File(playerDataDir, player.uuidAsString + ".dat").toPath()
70 | val backupFile = File(playerDataDir, player.uuidAsString + ".dat_old").toPath()
71 | Util.backupAndReplace(currentFile, newPlayerFile, backupFile)
72 | } catch (e: Exception) {
73 | WorldTools.LOG.warn("Failed to save player data for {}", player.name.string)
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/RegionBasedChunk.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.SharedConstants
4 | import net.minecraft.block.Block
5 | import net.minecraft.block.BlockState
6 | import net.minecraft.block.Blocks
7 | import net.minecraft.block.entity.BlockEntity
8 | import net.minecraft.fluid.Fluid
9 | import net.minecraft.nbt.NbtCompound
10 | import net.minecraft.nbt.NbtList
11 | import net.minecraft.nbt.NbtLongArray
12 | import net.minecraft.nbt.NbtOps
13 | import net.minecraft.registry.Registries
14 | import net.minecraft.registry.RegistryKeys
15 | import net.minecraft.text.MutableText
16 | import net.minecraft.util.math.BlockPos
17 | import net.minecraft.util.math.ChunkSectionPos
18 | import net.minecraft.world.LightType
19 | import net.minecraft.world.biome.BiomeKeys
20 | import net.minecraft.world.chunk.BelowZeroRetrogen
21 | import net.minecraft.world.chunk.PalettedContainer
22 | import net.minecraft.world.chunk.SerializedChunk
23 | import net.minecraft.world.chunk.WorldChunk
24 | import net.minecraft.world.gen.chunk.BlendingData
25 | import net.minecraft.world.level.storage.LevelStorage
26 | import org.waste.of.time.WorldTools.LOG
27 | import org.waste.of.time.WorldTools.TIMESTAMP_KEY
28 | import org.waste.of.time.WorldTools.config
29 | import org.waste.of.time.extension.IPalettedContainerExtension
30 | import org.waste.of.time.manager.MessageManager.translateHighlight
31 | import org.waste.of.time.manager.StatisticManager
32 | import org.waste.of.time.storage.Cacheable
33 | import org.waste.of.time.storage.CustomRegionBasedStorage
34 | import org.waste.of.time.storage.RegionBased
35 | import org.waste.of.time.storage.cache.HotCache
36 |
37 | open class RegionBasedChunk(
38 | val chunk: WorldChunk,
39 | ) : RegionBased(chunk.pos, chunk.world, "region"), Cacheable {
40 | // storing a reference to the block entities in the chunk to prevent them from being unloaded
41 | val cachedBlockEntities = mutableMapOf()
42 |
43 | init {
44 | cachedBlockEntities.putAll(chunk.blockEntities)
45 |
46 | cachedBlockEntities.values.associateWith { fresh ->
47 | HotCache.scannedBlockEntities[fresh.pos]
48 | }.forEach { (fresh, cached) ->
49 | if (cached == null) return@forEach
50 | cachedBlockEntities[fresh.pos] = cached
51 | }
52 | }
53 |
54 | override fun shouldStore() = config.general.capture.chunks
55 |
56 | override val verboseInfo: MutableText
57 | get() = translateHighlight(
58 | "worldtools.capture.saved.chunks",
59 | chunkPos,
60 | dimension
61 | )
62 |
63 | override val anonymizedInfo: MutableText
64 | get() = translateHighlight(
65 | "worldtools.capture.saved.chunks.anonymized",
66 | dimension
67 | )
68 |
69 | private val stateIdContainer = PalettedContainer.createPalettedContainerCodec(
70 | Block.STATE_IDS,
71 | BlockState.CODEC,
72 | PalettedContainer.PaletteProvider.BLOCK_STATE,
73 | Blocks.AIR.defaultState
74 | )
75 |
76 | override fun cache() {
77 | HotCache.chunks[chunkPos] = this
78 | HotCache.savedChunks.add(chunkPos.toLong())
79 | }
80 |
81 | override fun flush() {
82 | HotCache.chunks.remove(chunkPos)
83 | }
84 |
85 | override fun incrementStats() {
86 | StatisticManager.chunks++
87 | StatisticManager.dimensions.add(dimension)
88 | }
89 |
90 | override fun writeToStorage(
91 | session: LevelStorage.Session,
92 | storage: CustomRegionBasedStorage,
93 | cachedStorages: MutableMap
94 | ) {
95 | // avoiding `emit` here due to flow order issues when capture is stopped
96 | // i.e., if EndFlow is emitted before this,
97 | // these are not written because they're behind it in the flow
98 | HotCache.getEntitySerializableForChunk(chunkPos, world)
99 | ?.store(session, cachedStorages)
100 | ?: run {
101 | // remove any previously stored entities in this chunk in case there are no entities to store
102 | RegionBasedEntities(chunkPos, emptySet(), world).store(session, cachedStorages)
103 | }
104 | if (chunk.isEmpty) return
105 | super.writeToStorage(session, storage, cachedStorages)
106 | }
107 |
108 | /**
109 | * See [net.minecraft.world.ChunkSerializer.serialize]
110 | */
111 | override fun compound() = NbtCompound().apply {
112 | if (config.world.metadata.captureTimestamp) {
113 | putLong(TIMESTAMP_KEY, System.currentTimeMillis())
114 | }
115 |
116 | putInt("DataVersion", SharedConstants.getGameVersion().saveVersion.id)
117 | putInt(SerializedChunk.X_POS_KEY, chunk.pos.x)
118 | putInt("yPos", chunk.bottomSectionCoord)
119 | putInt(SerializedChunk.Z_POS_KEY, chunk.pos.z)
120 | putLong("LastUpdate", chunk.world.time)
121 | putLong("InhabitedTime", chunk.inhabitedTime)
122 | putString("Status", Registries.CHUNK_STATUS.getId(chunk.status).toString())
123 |
124 | genBackwardsCompat(chunk)
125 |
126 | if (!chunk.upgradeData.isDone) {
127 | put("UpgradeData", chunk.upgradeData.toNbt())
128 | }
129 |
130 | put(SerializedChunk.SECTIONS_KEY, generateSections(chunk))
131 |
132 | if (chunk.isLightOn) {
133 | putBoolean(SerializedChunk.IS_LIGHT_ON_KEY, true)
134 | }
135 |
136 | put("block_entities", NbtList().apply {
137 | upsertBlockEntities()
138 | })
139 |
140 | getTickSchedulers(chunk)
141 | genPostProcessing(chunk)
142 |
143 | // skip structures
144 | if (config.debug.logSavedChunks)
145 | LOG.info("Chunk saved: $chunkPos ($dimension)")
146 | }
147 |
148 | private fun NbtList.upsertBlockEntities() {
149 | cachedBlockEntities.entries.map { (_, blockEntity) ->
150 | blockEntity.createNbtWithIdentifyingData(world.registryManager).apply {
151 | putBoolean("keepPacked", false)
152 | }
153 | }.apply {
154 | addAll(this)
155 | }
156 | }
157 |
158 | private fun generateSections(chunk: WorldChunk) = NbtList().apply {
159 | val biomeRegistry = chunk.world.registryManager.getOptional(RegistryKeys.BIOME).orElse(null) ?: return@apply
160 | val defaultValue = biomeRegistry.getOptional(BiomeKeys.PLAINS).orElse(null) ?: return@apply
161 | val biomeCodec = PalettedContainer.createReadableContainerCodec(
162 | biomeRegistry.indexedEntries,
163 | biomeRegistry.entryCodec,
164 | PalettedContainer.PaletteProvider.BIOME,
165 | defaultValue
166 | )
167 | val lightingProvider = chunk.world.chunkManager.lightingProvider
168 |
169 | (lightingProvider.bottomY until lightingProvider.topY).forEach { y ->
170 | val sectionCoord = chunk.sectionCoordToIndex(y)
171 | val inSection = sectionCoord in (0 until chunk.sectionArray.size)
172 | val blockLightSection =
173 | lightingProvider[LightType.BLOCK].getLightSection(ChunkSectionPos.from(chunk.pos, y))
174 | val skyLightSection =
175 | lightingProvider[LightType.SKY].getLightSection(ChunkSectionPos.from(chunk.pos, y))
176 |
177 | if (!inSection && blockLightSection == null && skyLightSection == null) return@forEach
178 |
179 | add(NbtCompound().apply {
180 | if (inSection) {
181 | val chunkSection = chunk.sectionArray[sectionCoord]
182 | /**
183 | * Mods like Bobby may also try serializing chunk data concurrently on separate threads
184 | * PalettedContainer contains a lock that is acquired during read/write operations
185 | *
186 | * Force disabling checking the lock's status here as it should be safe to
187 | * read here, no write operations should happen after the chunk is unloaded
188 | */
189 | (chunkSection.blockStateContainer as IPalettedContainerExtension).setWTIgnoreLock(true)
190 | (chunkSection.biomeContainer as IPalettedContainerExtension).setWTIgnoreLock(true)
191 | put(
192 | "block_states",
193 | stateIdContainer.encodeStart(NbtOps.INSTANCE, chunkSection.blockStateContainer).getOrThrow()
194 | )
195 | put(
196 | "biomes",
197 | biomeCodec.encodeStart(NbtOps.INSTANCE, chunkSection.biomeContainer).getOrThrow()
198 | )
199 | (chunkSection.blockStateContainer as IPalettedContainerExtension).setWTIgnoreLock(false)
200 | (chunkSection.biomeContainer as IPalettedContainerExtension).setWTIgnoreLock(false)
201 | }
202 | if (blockLightSection != null && !blockLightSection.isUninitialized) {
203 | putByteArray(SerializedChunk.BLOCK_LIGHT_KEY, blockLightSection.asByteArray())
204 | }
205 | if (skyLightSection != null && !skyLightSection.isUninitialized) {
206 | putByteArray(SerializedChunk.SKY_LIGHT_KEY, skyLightSection.asByteArray())
207 | }
208 | if (isEmpty) return@forEach
209 | putByte("Y", y.toByte())
210 | })
211 | }
212 | }
213 |
214 | private fun NbtCompound.genBackwardsCompat(chunk: WorldChunk) {
215 | chunk.blendingData?.let { bleedingData ->
216 | BlendingData.Serialized.CODEC.encodeStart(NbtOps.INSTANCE, bleedingData.toSerialized()).resultOrPartial {
217 | LOG.error(it)
218 | }.ifPresent {
219 | put("blending_data", it)
220 | }
221 | }
222 |
223 | chunk.belowZeroRetrogen?.let { belowZeroRetrogen ->
224 | BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, belowZeroRetrogen).resultOrPartial {
225 | LOG.error(it)
226 | }.ifPresent {
227 | put("below_zero_retrogen", it)
228 | }
229 | }
230 | }
231 |
232 | private fun NbtCompound.getTickSchedulers(chunk: WorldChunk) {
233 | val time = chunk.world.levelProperties.time
234 | val tickSchedulers = chunk.getTickSchedulers(time)
235 |
236 | val blockTickSchedulers = tickSchedulers.blocks.map { ticker ->
237 | ticker.toNbt { Registries.BLOCK.getId(it).toString()}
238 | }
239 | put("block_ticks", NbtList().apply { addAll(blockTickSchedulers) })
240 | val fluidTickSchedulers = tickSchedulers.fluids.map { ticker ->
241 | ticker.toNbt { Registries.FLUID.getId(it).toString()}
242 | }
243 | put("fluid_ticks", NbtList().apply { addAll(fluidTickSchedulers) })
244 | }
245 |
246 | private fun NbtCompound.genPostProcessing(chunk: WorldChunk) {
247 | put("PostProcessing", SerializedChunk.toNbt(chunk.postProcessingLists))
248 |
249 | put(SerializedChunk.HEIGHTMAPS_KEY, NbtCompound().apply {
250 | chunk.heightmaps.filter {
251 | chunk.status.heightmapTypes.contains(it.key)
252 | }.forEach { (key, value) ->
253 | put(key.getName(), NbtLongArray(value.asLongArray()))
254 | }
255 | })
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/RegionBasedEntities.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import net.minecraft.SharedConstants
4 | import net.minecraft.nbt.NbtCompound
5 | import net.minecraft.nbt.NbtIntArray
6 | import net.minecraft.nbt.NbtList
7 | import net.minecraft.text.MutableText
8 | import net.minecraft.util.math.ChunkPos
9 | import net.minecraft.world.World
10 | import net.minecraft.world.level.storage.LevelStorage
11 | import org.waste.of.time.WorldTools.LOG
12 | import org.waste.of.time.WorldTools.config
13 | import org.waste.of.time.manager.MessageManager.translateHighlight
14 | import org.waste.of.time.manager.StatisticManager
15 | import org.waste.of.time.manager.StatisticManager.joinWithAnd
16 | import org.waste.of.time.storage.CustomRegionBasedStorage
17 | import org.waste.of.time.storage.RegionBased
18 | import org.waste.of.time.storage.cache.EntityCacheable
19 |
20 | class RegionBasedEntities(
21 | chunkPos: ChunkPos,
22 | val entities: Set, // can be empty, signifies we should clear any previously saved entities
23 | world: World
24 | ) : RegionBased(chunkPos, world, "entities") {
25 | override fun shouldStore() = config.general.capture.entities
26 |
27 | override val verboseInfo: MutableText
28 | get() = translateHighlight(
29 | "worldtools.capture.saved.entities",
30 | stackEntities(),
31 | chunkPos,
32 | dimension
33 | )
34 |
35 | override val anonymizedInfo: MutableText
36 | get() = translateHighlight(
37 | "worldtools.capture.saved.entities.anonymized",
38 | stackEntities(),
39 | dimension
40 | )
41 |
42 | override fun compound() = NbtCompound().apply {
43 | put("Entities", NbtList().apply {
44 | entities.forEach { entity ->
45 | add(entity.compound())
46 | }
47 | })
48 |
49 | putInt("DataVersion", SharedConstants.getGameVersion().saveVersion.id)
50 | put("Position", NbtIntArray(intArrayOf(chunkPos.x, chunkPos.z)))
51 | if (config.debug.logSavedEntities) {
52 | entities.forEach { entity -> LOG.info("Entity saved: $entity (Chunk: $chunkPos)") }
53 | }
54 | }
55 |
56 | override fun writeToStorage(
57 | session: LevelStorage.Session,
58 | storage: CustomRegionBasedStorage,
59 | cachedStorages: MutableMap
60 | ) {
61 | if (entities.isEmpty()) {
62 | // remove any previously stored entities in this chunk
63 | if (config.debug.logSavedEntities) {
64 | LOG.info("Removing any previously saved entities from chunk: {}", chunkPos)
65 | }
66 | storage.write(chunkPos, null)
67 | return
68 | }
69 | super.writeToStorage(session, storage, cachedStorages)
70 | }
71 |
72 | override fun incrementStats() {
73 | // todo: entities count becomes completely wrong when entities are removed or are loaded twice during the capture
74 | // i.e. the player moves away, unloads them, and then comes back to load them again
75 | StatisticManager.entities += entities.size
76 | StatisticManager.dimensions.add(dimension)
77 | }
78 |
79 | private fun stackEntities() = entities.groupBy {
80 | it.entity.name
81 | }.map {
82 | val count = if (it.value.size > 1) {
83 | " (${it.value.size})"
84 | } else ""
85 | it.key.copy().append(count)
86 | }.joinWithAnd()
87 | }
88 |
--------------------------------------------------------------------------------
/common/src/main/kotlin/org/waste/of/time/storage/serializable/StatisticStoreable.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.storage.serializable
2 |
3 | import com.google.gson.JsonObject
4 | import net.minecraft.registry.Registries
5 | import net.minecraft.text.MutableText
6 | import net.minecraft.util.PathUtil
7 | import net.minecraft.util.WorldSavePath
8 | import net.minecraft.world.level.storage.LevelStorage
9 | import org.waste.of.time.manager.MessageManager.translateHighlight
10 | import org.waste.of.time.WorldTools
11 | import org.waste.of.time.WorldTools.CURRENT_VERSION
12 | import org.waste.of.time.WorldTools.GSON
13 | import org.waste.of.time.WorldTools.config
14 | import org.waste.of.time.WorldTools.mc
15 | import org.waste.of.time.storage.Storeable
16 | import org.waste.of.time.storage.CustomRegionBasedStorage
17 | import java.nio.charset.StandardCharsets
18 | import java.nio.file.Files
19 |
20 | class StatisticStoreable : Storeable() {
21 | override fun shouldStore() = config.general.capture.statistics
22 |
23 | override val verboseInfo: MutableText
24 | get() = translateHighlight(
25 | "worldtools.capture.saved.statistics",
26 | mc.player?.name ?: "Unknown"
27 | )
28 |
29 | override val anonymizedInfo: MutableText
30 | get() = verboseInfo
31 |
32 | override fun store(session: LevelStorage.Session, cachedStorages: MutableMap) {
33 | // we need to get the stat map from the player's stat handler instead of the packet because the packet only
34 | // contains the stats that have changed since the last time the packet was sent
35 | val completeStatMap = mc.player?.statHandler?.statMap?.toMap() ?: return
36 | val uuid = mc.player?.uuid ?: return
37 | val statDirectory = session.getDirectory(WorldSavePath.STATS)
38 |
39 | val json = JsonObject().apply {
40 | addProperty("Author", WorldTools.CREDIT_MESSAGE)
41 | add("stats", JsonObject().apply {
42 | completeStatMap.entries.groupBy { it.key.type }.forEach { (type, entries) ->
43 | val typeObject = JsonObject()
44 | entries.forEach { (stat, value) ->
45 | stat.name.split(":").getOrNull(1)?.replace('.', ':')?.let {
46 | typeObject.addProperty(it, value)
47 | }
48 | }
49 | add(Registries.STAT_TYPE.getId(type).toString(), typeObject)
50 | }
51 | })
52 | addProperty("DataVersion", CURRENT_VERSION)
53 | }
54 |
55 | PathUtil.createDirectories(statDirectory)
56 | Files.newBufferedWriter(
57 | statDirectory.resolve("$uuid.json"),
58 | StandardCharsets.UTF_8
59 | ).use { writer ->
60 | GSON.toJson(json, writer)
61 | }
62 |
63 | WorldTools.LOG.info("Saved ${completeStatMap.entries.size} stats.")
64 | }
65 | }
--------------------------------------------------------------------------------
/common/src/main/resources/assets/worldtools/WorldTools.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avanatiker/WorldTools/f82b7415c5a5fab619cc8f537f36defdea09c546/common/src/main/resources/assets/worldtools/WorldTools.png
--------------------------------------------------------------------------------
/common/src/main/resources/assets/worldtools/lang/en_pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "text.autoconfig.worldtools.category.Advanced": "Ol’ Coat",
3 | "text.autoconfig.worldtools.category.Debug": "Fixin' the Sails",
4 | "text.autoconfig.worldtools.category.Entity": "Sharks",
5 | "text.autoconfig.worldtools.category.General": "On Deck",
6 | "text.autoconfig.worldtools.category.Render": "Yo Ho!",
7 | "text.autoconfig.worldtools.category.World": "Sea",
8 | "text.autoconfig.worldtools.option.advanced": "Ol’ Coat",
9 | "text.autoconfig.worldtools.option.advanced.anonymousMode": "Hide yo booty!",
10 | "text.autoconfig.worldtools.option.advanced.anonymousMode.@Tooltip": "Rid leakage o' yer ship. Ye Be Warned: This does not affect the actual data!",
11 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui": "Hide Exploratory Sea GUI",
12 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui.@Tooltip": "Lonely pirates be no longer warned.",
13 | "text.autoconfig.worldtools.option.advanced.keepEnderChestContents": "Keep ye ol’ Ender Chest Contents between Captures",
14 | "text.autoconfig.worldtools.option.advanced.showChatMessages": "Hidden Banter",
15 | "text.autoconfig.worldtools.option.advanced.showToasts": "Show yer Toast",
16 | "text.autoconfig.worldtools.option.debug": "Repairs",
17 | "text.autoconfig.worldtools.option.debug.@PrefixText": "Enabling repairs options will make it easier to find issues, using the log file located at .minecraft/logs/latest.log",
18 | "text.autoconfig.worldtools.option.debug.logSavedChunks": "Map ye Waves",
19 | "text.autoconfig.worldtools.option.debug.logSavedContainers": "Map ye Treasure Chests",
20 | "text.autoconfig.worldtools.option.debug.logSavedEntities": "Map ye Saved Sharks",
21 | "text.autoconfig.worldtools.option.debug.logSavedMaps": "Map the Maps",
22 | "text.autoconfig.worldtools.option.debug.logSettings": "Map Scribble",
23 | "text.autoconfig.worldtools.option.debug.logZippingProgress": "Map Booty Locking Progress",
24 | "text.autoconfig.worldtools.option.entity": "Sharks",
25 | "text.autoconfig.worldtools.option.entity.behavior": "Shark Behavior",
26 | "text.autoconfig.worldtools.option.entity.behavior.invulnerable": "Dead Men Tell Tales",
27 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior": "Modify Shark Behavior",
28 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior.@Tooltip": "When enabled, the enabled tags will be added t’ the NBT. Otherwise, the shark behavior will be left to the fishes.",
29 | "text.autoconfig.worldtools.option.entity.behavior.noAI": "Shark be without mind",
30 | "text.autoconfig.worldtools.option.entity.behavior.noGravity": "FLOATING SHARKS",
31 | "text.autoconfig.worldtools.option.entity.behavior.silent": "Belay yer mouth",
32 | "text.autoconfig.worldtools.option.entity.censor": "Belay",
33 | "text.autoconfig.worldtools.option.entity.censor.censorNames": "Belay Names",
34 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation": "Belay last death location",
35 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation.@Tooltip": "When enabled, the last death location o’ the player will not be saved t’ disk. Prevents leaking of coordinates.",
36 | "text.autoconfig.worldtools.option.entity.censor.names": "Belay Shark Name",
37 | "text.autoconfig.worldtools.option.entity.censor.owner": "Belay Shark Owner",
38 | "text.autoconfig.worldtools.option.entity.metadata": "Maps of maps",
39 | "text.autoconfig.worldtools.option.entity.metadata.captureTimestamp": "Note the sun",
40 | "text.autoconfig.worldtools.option.entity.metadata.waterMark": "Raise the Sails!",
41 | "text.autoconfig.worldtools.option.general.autoDownload": "Automatically map as ye start ye voyage",
42 | "text.autoconfig.worldtools.option.general.autoDownload.@Tooltip": "As ye embark on the sea, a capture’ll commence",
43 | "text.autoconfig.worldtools.option.general.capture": "What be mapped",
44 | "text.autoconfig.worldtools.option.general.capture.@Tooltip": "When be disabled, the corresponding data will not be saved t’ disk. This could result in incomplete captures!",
45 | "text.autoconfig.worldtools.option.general.capture.advancements": "Accomplishments",
46 | "text.autoconfig.worldtools.option.general.capture.chunks": "Waves",
47 | "text.autoconfig.worldtools.option.general.capture.entities": "Sharks",
48 | "text.autoconfig.worldtools.option.general.capture.levelData": "Level.dat",
49 | "text.autoconfig.worldtools.option.general.capture.maps": "Ye ol' Maps",
50 | "text.autoconfig.worldtools.option.general.capture.metadata": "Maps of maps",
51 | "text.autoconfig.worldtools.option.general.capture.players": "Pirates",
52 | "text.autoconfig.worldtools.option.general.capture.statistics": "Ship’s manifest",
53 | "text.autoconfig.worldtools.option.general.compressLevel": "Store capture in Armory",
54 | "text.autoconfig.worldtools.option.general.compressLevel.@Tooltip": "When a capture be at thar end, it will be archived in ye armory",
55 | "text.autoconfig.worldtools.option.render": "Yo Ho!",
56 | "text.autoconfig.worldtools.option.render.accentColor": "Accent Color (AaaaarGB Hex)",
57 | "text.autoconfig.worldtools.option.render.captureBarColor": "Capture Info Bar Color (AaaaarGB Hex)",
58 | "text.autoconfig.worldtools.option.render.captureBarStyle": "Capture Info B’aaaaar Style",
59 | "text.autoconfig.worldtools.option.render.containerColor": "Missing Treasure Chest Color (AaaaarGB Hex)",
60 | "text.autoconfig.worldtools.option.render.progressBarColor": "Progress B’aaaaar Color",
61 | "text.autoconfig.worldtools.option.render.progressBarStyle": "Progress B’aaaaar Style",
62 | "text.autoconfig.worldtools.option.render.progressBarTimeout": "Progress B’aaaaar Timeout (ms)",
63 | "text.autoconfig.worldtools.option.render.renderNotYetCachedContainers": "Treasure unchecked",
64 | "text.autoconfig.worldtools.option.world.censor": "Belay",
65 | "text.autoconfig.worldtools.option.world.gameRules": "There be Rules",
66 | "text.autoconfig.worldtools.option.world.gameRules.doDaylightCycle": "Day by Day",
67 | "text.autoconfig.worldtools.option.world.gameRules.doFireTick": "Fire on the Ship",
68 | "text.autoconfig.worldtools.option.world.gameRules.doMobGriefing": "Mob Piracy",
69 | "text.autoconfig.worldtools.option.world.gameRules.doMobSpawning": "Mob’s Aaaaar Here",
70 | "text.autoconfig.worldtools.option.world.gameRules.doPatrolSpawning": "Patrol’s Aaaaar Here",
71 | "text.autoconfig.worldtools.option.world.gameRules.doTraderSpawning": "Trader Aaaaar Here",
72 | "text.autoconfig.worldtools.option.world.gameRules.doVinesSpread": "Barnacle Spread",
73 | "text.autoconfig.worldtools.option.world.gameRules.doWardenSpawning": "Kraken Spawning",
74 | "text.autoconfig.worldtools.option.world.gameRules.doWeatherCycle": "Storms n Clear",
75 | "text.autoconfig.worldtools.option.world.gameRules.keepInventory": "Keep ye Loot",
76 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules": "Modify Ship Rules",
77 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules.@Tooltip": "When enabled, ye ship rules will be changed to the values set in this config. Otherwise, the ship rules will be left untouched.",
78 | "text.autoconfig.worldtools.option.world.metadata": "Maps of maps",
79 | "text.autoconfig.worldtools.option.world.metadata.captureTimestamp": "Note Timestamp o’ Capture",
80 | "text.autoconfig.worldtools.option.world.metadata.waterMark": "Watermark Waves",
81 | "text.autoconfig.worldtools.option.world.worldGenerator": "Sea Generator",
82 | "text.autoconfig.worldtools.option.world.worldGenerator.bonusChest": "Bonus Booty",
83 | "text.autoconfig.worldtools.option.world.worldGenerator.generateFeatures": "Generate ye Features",
84 | "text.autoconfig.worldtools.option.world.worldGenerator.seed": "Sea Chart",
85 | "text.autoconfig.worldtools.option.world.worldGenerator.type": "Sea Generate Type",
86 | "text.autoconfig.worldtools.option.world.worldGenerator.type.@Tooltip": "For inference: VOID: Will generate an empty sea. FLAT: Will generate a flat sea. DEFAULT: Will generate a normal sea.",
87 | "text.autoconfig.worldtools.title": "SeaTools Config",
88 | "worldtools.capture.and": " n’ ",
89 | "worldtools.capture.chunk": "%s wave",
90 | "worldtools.capture.chunks": "%s waves",
91 | "worldtools.capture.click_to_open": "%s (click t’ open)",
92 | "worldtools.capture.container": "%s Treasure Chests",
93 | "worldtools.capture.containers": "%s Treasure Chests",
94 | "worldtools.capture.entities": "%s sharks",
95 | "worldtools.capture.entity": "%s shark",
96 | "worldtools.capture.in_dimension": " in width n’ length ",
97 | "worldtools.capture.nothing_saved_yet": "Nothing mapped yet for %s",
98 | "worldtools.capture.player": "%s pirates",
99 | "worldtools.capture.players": "%s pirates",
100 | "worldtools.capture.saved": "Mapped ",
101 | "worldtools.capture.saved.advancements": "Saved Accomplishments o’ %s",
102 | "worldtools.capture.saved.chunks": "Saved wave at %s in dimension %s",
103 | "worldtools.capture.saved.chunks.anonymized": "Saved wave in dimension %s",
104 | "worldtools.capture.saved.compressed": "Saved Booty Lock o’ capture as %s",
105 | "worldtools.capture.saved.end_flow": "Finished capture o’ %s",
106 | "worldtools.capture.saved.entities": "Saved %s in wave %s in ye dimension %s",
107 | "worldtools.capture.saved.entities.anonymized": "Saved %s in ye dimension %s",
108 | "worldtools.capture.saved.levelData": "Saved %s o’ %s",
109 | "worldtools.capture.saved.mapData": "Saved maps in ye dimension %s",
110 | "worldtools.capture.saved.metadata": "Saved maps of maps o’ %s",
111 | "worldtools.capture.saved.player": "Saved pirate %s at %s in dimension %s",
112 | "worldtools.capture.saved.player.anonymized": "Saved pirate %s in dimension %s",
113 | "worldtools.capture.saved.statistics": "Saved Ship’s manifest o’ %s",
114 | "worldtools.capture.to_directory": " t’ saves log ",
115 | "worldtools.capture.took": " (plundered %s)",
116 | "worldtools.gui.browser.title": "SeaTools Browser",
117 | "worldtools.gui.capture.existing_world_confirm.message": "Ye already made: \"%s\". Write over it? (Will merge with existing data)",
118 | "worldtools.gui.capture.existing_world_confirm.title": "Already ventured that Sea",
119 | "worldtools.gui.escape.button.finish_download": "Save capture o’ %s",
120 | "worldtools.gui.manager.button.cancel": "Avast",
121 | "worldtools.gui.manager.button.config": "Scribble",
122 | "worldtools.gui.manager.button.start_download": "Start the mappin’ ",
123 | "worldtools.gui.manager.button.stop_download": "Stop the mappin’ ",
124 | "worldtools.gui.manager.title": "SeaTools Manager",
125 | "worldtools.gui.manager.world_name_placeholder": "Enter sea name... (default: %s)",
126 | "worldtools.key.categories": "SeaTools",
127 | "worldtools.key.open_config": "Open Scribble",
128 | "worldtools.key.toggle_capture": "Quick mappin’",
129 | "worldtools.log.error.already_capturing": "A capture for %s has already set sail!",
130 | "worldtools.log.error.failed_to_create_session": "Failed t’ create session for \"%s\"! (error: %s)",
131 | "worldtools.log.error.failed_to_save_level": "Failed t’ save level.dat for \"%s\"! (error: %s)",
132 | "worldtools.log.error.failed_to_visit_file": "Failed accessing file %s t’ create a Booty Lock! (error: %s)",
133 | "worldtools.log.error.failed_to_zip": "Failed t’ create session t’ Booty Lock capture o’ %s! (error: %s)",
134 | "worldtools.log.error.not_capturing": "No capture be running yet!",
135 | "worldtools.log.error.world_name_too_long": "Sea name \"%s\" is too long! (max %d characters)",
136 | "worldtools.log.info.singleplayer_capture": "EXPLORATORY SHIPS: Capturing in single-pirate is experimental and may not work as expected!",
137 | "worldtools.log.info.started_capture": "Started mappin’ %s...",
138 | "worldtools.log.info.stopping_capture": "Stopping mappin’ %s..."
139 | }
140 |
--------------------------------------------------------------------------------
/common/src/main/resources/assets/worldtools/lang/zh_cn.json:
--------------------------------------------------------------------------------
1 | {
2 | "text.autoconfig.worldtools.category.Advanced": "高级",
3 | "text.autoconfig.worldtools.category.Debug": "调试",
4 | "text.autoconfig.worldtools.category.Entity": "实体",
5 | "text.autoconfig.worldtools.category.General": "通用",
6 | "text.autoconfig.worldtools.category.Render": "渲染",
7 | "text.autoconfig.worldtools.category.World": "世界",
8 | "text.autoconfig.worldtools.option.advanced": "高级",
9 | "text.autoconfig.worldtools.option.advanced.anonymousMode": "在界面中隐藏坐标",
10 | "text.autoconfig.worldtools.option.advanced.anonymousMode.@Tooltip": "启用后,捕获的坐标将在界面中隐藏。注意:这不会影响实际数据!",
11 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui": "隐藏实验性世界GUI",
12 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui.@Tooltip": "启用后,在单人游戏中加载世界后烦人的弹出窗口将被隐藏。",
13 | "text.autoconfig.worldtools.option.advanced.keepEnderChestContents": "在捕获之间保留末影箱内容",
14 | "text.autoconfig.worldtools.option.advanced.showChatMessages": "在聊天栏中显示信息",
15 | "text.autoconfig.worldtools.option.advanced.showToasts": "展示Toasts",
16 | "text.autoconfig.worldtools.option.debug": "调试",
17 | "text.autoconfig.worldtools.option.debug.@PrefixText": "启用调试选项将使日志文件更易找到问题",
18 | "text.autoconfig.worldtools.option.debug.logSavedChunks": "记录保存的区块",
19 | "text.autoconfig.worldtools.option.debug.logSavedContainers": "记录保存的容器",
20 | "text.autoconfig.worldtools.option.debug.logSavedEntities": "记录保存的实体",
21 | "text.autoconfig.worldtools.option.debug.logSavedMaps": "记录保存的地图",
22 | "text.autoconfig.worldtools.option.debug.logSettings": "记录设置",
23 | "text.autoconfig.worldtools.option.debug.logZippingProgress": "记录压缩进度",
24 | "text.autoconfig.worldtools.option.entity": "实体",
25 | "text.autoconfig.worldtools.option.entity.behavior": "行为",
26 | "text.autoconfig.worldtools.option.entity.behavior.invulnerable": "无敌",
27 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior": "修改实体行为",
28 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior.@Tooltip": "启用后,启用的标签将添加到 NBT。否则,实体行为将保持不变。",
29 | "text.autoconfig.worldtools.option.entity.behavior.noAI": "无 AI",
30 | "text.autoconfig.worldtools.option.entity.behavior.noGravity": "无重力",
31 | "text.autoconfig.worldtools.option.entity.behavior.silent": "无声音",
32 | "text.autoconfig.worldtools.option.entity.censor": "隐藏",
33 | "text.autoconfig.worldtools.option.entity.censor.censorNames": "隐藏名称",
34 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation": "隐藏最后死亡坐标",
35 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation.@Tooltip": "启用后,玩家的最后死亡位置将不会保存到磁盘上。防止坐标泄露。",
36 | "text.autoconfig.worldtools.option.entity.censor.names": "隐藏实体名称",
37 | "text.autoconfig.worldtools.option.entity.censor.owner": "隐藏实体主人",
38 | "text.autoconfig.worldtools.option.entity.metadata": "元数据",
39 | "text.autoconfig.worldtools.option.entity.metadata.captureTimestamp": "捕获时间戳",
40 | "text.autoconfig.worldtools.option.entity.metadata.waterMark": "水印",
41 | "text.autoconfig.worldtools.option.general.autoDownload": "在加入游戏时自动开始捕获",
42 | "text.autoconfig.worldtools.option.general.autoDownload.@Tooltip": "当你加入一个世界,就开始捕获",
43 | "text.autoconfig.worldtools.option.general.capture": "什么应该被捕获",
44 | "text.autoconfig.worldtools.option.general.capture.@Tooltip": "禁用后,相应的数据将不会保存到本地。这可能会导致捕获不完整!",
45 | "text.autoconfig.worldtools.option.general.capture.advancements": "成就",
46 | "text.autoconfig.worldtools.option.general.capture.chunks": "区块",
47 | "text.autoconfig.worldtools.option.general.capture.entities": "实体",
48 | "text.autoconfig.worldtools.option.general.capture.levelData": "Level.dat",
49 | "text.autoconfig.worldtools.option.general.capture.maps": "地图",
50 | "text.autoconfig.worldtools.option.general.capture.metadata": "元数据",
51 | "text.autoconfig.worldtools.option.general.capture.players": "玩家",
52 | "text.autoconfig.worldtools.option.general.capture.statistics": "统计信息",
53 | "text.autoconfig.worldtools.option.general.compressLevel": "将捕获存档保存为ZIP压缩文件",
54 | "text.autoconfig.worldtools.option.general.compressLevel.@Tooltip": "捕获完成后,它将作为ZIP压缩文件存档",
55 | "text.autoconfig.worldtools.option.render": "渲染",
56 | "text.autoconfig.worldtools.option.render.accentColor": "主题色(RGB 十六进制)",
57 | "text.autoconfig.worldtools.option.render.captureBarColor": "捕获信息栏颜色(RGB 十六进制)",
58 | "text.autoconfig.worldtools.option.render.captureBarStyle": "捕获信息栏样式",
59 | "text.autoconfig.worldtools.option.render.unscannedContainerColor": "缺失容器颜色(RGB 十六进制)",
60 | "text.autoconfig.worldtools.option.render.fromCacheLoadedContainerColor": "从缓存加载的容器颜色(RGB 十六进制)",
61 | "text.autoconfig.worldtools.option.render.unscannedEntityColor": "缺失实体颜色(RGB 十六进制)",
62 | "text.autoconfig.worldtools.option.render.fromCacheLoadedEntityColor": "从缓存加载的实体颜色(RGB 十六进制)",
63 | "text.autoconfig.worldtools.option.render.progressBarColor": "进度条颜色",
64 | "text.autoconfig.worldtools.option.render.progressBarStyle": "进度条样式",
65 | "text.autoconfig.worldtools.option.render.progressBarTimeout": "进度条耗时(毫秒)",
66 | "text.autoconfig.worldtools.option.render.renderNotYetCachedContainers": "呈现尚未缓存的容器",
67 | "text.autoconfig.worldtools.option.world.censor": "隐藏",
68 | "text.autoconfig.worldtools.option.world.gameRules": "游戏规则",
69 | "text.autoconfig.worldtools.option.world.gameRules.doDaylightCycle": "时间流逝",
70 | "text.autoconfig.worldtools.option.world.gameRules.doFireTick": "火势蔓延",
71 | "text.autoconfig.worldtools.option.world.gameRules.doMobGriefing": "生物破坏",
72 | "text.autoconfig.worldtools.option.world.gameRules.doMobSpawning": "生物生成",
73 | "text.autoconfig.worldtools.option.world.gameRules.doPatrolSpawning": "灾厄巡逻队生成",
74 | "text.autoconfig.worldtools.option.world.gameRules.doTraderSpawning": "流浪商人生成",
75 | "text.autoconfig.worldtools.option.world.gameRules.doVinesSpread": "藤蔓生长",
76 | "text.autoconfig.worldtools.option.world.gameRules.doWardenSpawning": "撅首者生成",
77 | "text.autoconfig.worldtools.option.world.gameRules.doWeatherCycle": "天气更替",
78 | "text.autoconfig.worldtools.option.world.gameRules.keepInventory": "保留物品栏",
79 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules": "修改游戏规则",
80 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules.@Tooltip": "启用后,游戏规则将修改为此配置中设置的值。否则,游戏规则将保持不变。",
81 | "text.autoconfig.worldtools.option.world.metadata": "元数据",
82 | "text.autoconfig.worldtools.option.world.metadata.captureTimestamp": "存储捕获的时间戳",
83 | "text.autoconfig.worldtools.option.world.metadata.waterMark": "区块水印",
84 | "text.autoconfig.worldtools.option.world.worldGenerator": "世界生成",
85 | "text.autoconfig.worldtools.option.world.worldGenerator.bonusChest": "奖励箱",
86 | "text.autoconfig.worldtools.option.world.worldGenerator.generateFeatures": "生成结构",
87 | "text.autoconfig.worldtools.option.world.worldGenerator.seed": "种子",
88 | "text.autoconfig.worldtools.option.world.worldGenerator.type": "世界生成类型",
89 | "text.autoconfig.worldtools.option.world.worldGenerator.type.@Tooltip": "VOID: 将生成一个空世界。 FLAT: 将生成一个超平坦世界 DEFAULT: 生成一个正常的世界。",
90 | "text.autoconfig.worldtools.title": "WorldTools 配置",
91 | "worldtools.capture.and": " 和 ",
92 | "worldtools.capture.chunk": "%s 区块",
93 | "worldtools.capture.chunks": "%s 区块",
94 | "worldtools.capture.click_to_open": "%s (点击打开)",
95 | "worldtools.capture.container": "%s 容器",
96 | "worldtools.capture.containers": "%s 容器",
97 | "worldtools.capture.entities": "%s 实体",
98 | "worldtools.capture.entity": "%s 实体",
99 | "worldtools.capture.in_dimension": " 在维度 ",
100 | "worldtools.capture.loaded.block_entities": "从缓存中加载了维度 %s 中区块 %s 的方块实体",
101 | "worldtools.capture.loaded.block_entities.anonymized": "从缓存中加载了维度 %s 中的方块实体",
102 | "worldtools.capture.nothing_saved_yet": "对于 %s 目前没有保存任何内容",
103 | "worldtools.capture.player": "%s 玩家",
104 | "worldtools.capture.players": "%s 玩家",
105 | "worldtools.capture.saved": "保存 ",
106 | "worldtools.capture.saved.advancements": "保存了 %s 的成就",
107 | "worldtools.capture.saved.chunks": "保存了区块 %s 在 %s 维度",
108 | "worldtools.capture.saved.chunks.anonymized": "在 %s 维度保存区块",
109 | "worldtools.capture.saved.compressed": "将捕获保存为ZIP压缩文件 %s",
110 | "worldtools.capture.saved.end_flow": "完成了 %s 的捕获",
111 | "worldtools.capture.saved.entities": "保存了 %s 在 %s 区块 在 %s 维度",
112 | "worldtools.capture.saved.entities.anonymized": "保存了 %s 在 %s 维度",
113 | "worldtools.capture.saved.levelData": "保存了 %s 关于 %s",
114 | "worldtools.capture.saved.mapData": "在 %s 维度保存了地图",
115 | "worldtools.capture.saved.metadata": "保存了 %s 的元数据",
116 | "worldtools.capture.saved.player": "保存了玩家 %s 在 %s 在 %s 维度",
117 | "worldtools.capture.saved.player.anonymized": "保存了玩家 %s 在 %s 在 %s 维度",
118 | "worldtools.capture.saved.statistics": "保存了 %s 的统计信息",
119 | "worldtools.capture.to_directory": " 到 saves 目录 ",
120 | "worldtools.capture.took": " (花费 %s)",
121 | "worldtools.gui.browser.title": "WorldTools 浏览器",
122 | "worldtools.gui.capture.existing_world_confirm.message": "名为: \"%s\" 的存档已存在。是否覆盖? (将合并已存在的数据)",
123 | "worldtools.gui.capture.existing_world_confirm.title": "发现已存在的世界",
124 | "worldtools.gui.escape.button.finish_download": "保存了 %s 的捕获",
125 | "worldtools.gui.manager.button.cancel": "取消",
126 | "worldtools.gui.manager.button.config": "配置",
127 | "worldtools.gui.manager.button.start_download": "开始下载",
128 | "worldtools.gui.manager.button.stop_download": "停止下载",
129 | "worldtools.gui.manager.title": "WorldTools 管理",
130 | "worldtools.gui.manager.world_name_placeholder": "输入世界名称… (默认: %s)",
131 | "worldtools.key.categories": "WorldTools",
132 | "worldtools.key.open_config": "打开配置",
133 | "worldtools.key.toggle_capture": "快速下载",
134 | "worldtools.log.error.already_capturing": "关于 %s 的捕获正在进行中!",
135 | "worldtools.log.error.failed_to_create_session": "无法为 \"%s\" 创建会话! (错误: %s)",
136 | "worldtools.log.error.failed_to_save_level": "无法为 \"%s\" 保存level.dat! (错误: %s)",
137 | "worldtools.log.error.failed_to_visit_file": "无法访问 %s 文件以创建ZIP压缩文件! (错误: %s)",
138 | "worldtools.log.error.failed_to_zip": "无法为会话创建压缩文件 %s! (错误: %s)",
139 | "worldtools.log.error.not_capturing": "目前没有捕获正在进行!",
140 | "worldtools.log.error.world_name_too_long": "世界名称 \"%s\" 太长了! (最长 %d 字符)",
141 | "worldtools.log.info.singleplayer_capture": "实验性:单人游戏中的捕获是实验性的,可能无法按预期工作!",
142 | "worldtools.log.info.started_capture": "开始捕获 %s ...",
143 | "worldtools.log.info.stopping_capture": "停止捕获 %s..."
144 | }
--------------------------------------------------------------------------------
/common/src/main/resources/assets/worldtools/lang/zh_tw.json:
--------------------------------------------------------------------------------
1 | {
2 | "text.autoconfig.worldtools.category.Advanced": "進階",
3 | "text.autoconfig.worldtools.category.Debug": "除錯",
4 | "text.autoconfig.worldtools.category.Entity": "實體",
5 | "text.autoconfig.worldtools.category.General": "一般",
6 | "text.autoconfig.worldtools.category.Render": "繪製",
7 | "text.autoconfig.worldtools.category.World": "世界",
8 | "text.autoconfig.worldtools.option.advanced": "進階",
9 | "text.autoconfig.worldtools.option.advanced.anonymousMode": "在介面中匿名化座標",
10 | "text.autoconfig.worldtools.option.advanced.anonymousMode.@Tooltip": "啟用後,擷取的座標將在介面中匿名化。注意:這不會影響實際資料!",
11 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui": "隱藏實驗性世界 GUI",
12 | "text.autoconfig.worldtools.option.advanced.hideExperimentalWorldGui.@Tooltip": "啟用後,在單人遊戲中載入世界後出現的惱人彈出視窗將會被隱藏。",
13 | "text.autoconfig.worldtools.option.advanced.keepEnderChestContents": "在擷取之間保留終界箱內容物",
14 | "text.autoconfig.worldtools.option.advanced.showChatMessages": "資訊聊天訊息",
15 | "text.autoconfig.worldtools.option.advanced.showToasts": "顯示通知訊息",
16 | "text.autoconfig.worldtools.option.debug": "除錯",
17 | "text.autoconfig.worldtools.option.debug.@PrefixText": "啟用除錯選項將會更容易找到問題,使用位於 .minecraft/logs/latest.log 的記錄檔。",
18 | "text.autoconfig.worldtools.option.debug.logSavedChunks": "記錄已儲存的區塊",
19 | "text.autoconfig.worldtools.option.debug.logSavedContainers": "記錄已儲存的容器",
20 | "text.autoconfig.worldtools.option.debug.logSavedEntities": "記錄已儲存的實體",
21 | "text.autoconfig.worldtools.option.debug.logSavedMaps": "記錄已儲存的地圖",
22 | "text.autoconfig.worldtools.option.debug.logSettings": "記錄設定",
23 | "text.autoconfig.worldtools.option.debug.logZippingProgress": "記錄壓縮進度",
24 | "text.autoconfig.worldtools.option.entity": "實體",
25 | "text.autoconfig.worldtools.option.entity.behavior": "行為",
26 | "text.autoconfig.worldtools.option.entity.behavior.invulnerable": "無敵",
27 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior": "修改實體行為",
28 | "text.autoconfig.worldtools.option.entity.behavior.modifyEntityBehavior.@Tooltip": "啟用後,已啟用的標籤將會被加入到 NBT。否則,實體行為將會保持不變。",
29 | "text.autoconfig.worldtools.option.entity.behavior.noAI": "無 AI",
30 | "text.autoconfig.worldtools.option.entity.behavior.noGravity": "無重力",
31 | "text.autoconfig.worldtools.option.entity.behavior.silent": "靜默",
32 | "text.autoconfig.worldtools.option.entity.censor": "隱藏",
33 | "text.autoconfig.worldtools.option.entity.censor.censorNames": "隱藏名稱",
34 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation": "隱藏最後死亡位置",
35 | "text.autoconfig.worldtools.option.entity.censor.lastDeathLocation.@Tooltip": "啟用後,玩家的最後死亡位置將不會被儲存到硬碟。防止座標洩漏。",
36 | "text.autoconfig.worldtools.option.entity.censor.names": "隱藏實體名稱",
37 | "text.autoconfig.worldtools.option.entity.censor.owner": "隱藏實體擁有者",
38 | "text.autoconfig.worldtools.option.entity.metadata": "中繼資料",
39 | "text.autoconfig.worldtools.option.entity.metadata.captureTimestamp": "擷取時間戳記",
40 | "text.autoconfig.worldtools.option.entity.metadata.waterMark": "浮水印",
41 | "text.autoconfig.worldtools.option.general.autoDownload": "加入時自動開始下載",
42 | "text.autoconfig.worldtools.option.general.autoDownload.@Tooltip": "當您加入世界時,將會開始擷取。",
43 | "text.autoconfig.worldtools.option.general.capture": "應該擷取什麼",
44 | "text.autoconfig.worldtools.option.general.capture.@Tooltip": "停用後,相應的資料將不會被儲存到硬碟。這可能會導致擷取不完整!",
45 | "text.autoconfig.worldtools.option.general.capture.advancements": "進度",
46 | "text.autoconfig.worldtools.option.general.capture.chunks": "區塊",
47 | "text.autoconfig.worldtools.option.general.capture.entities": "實體",
48 | "text.autoconfig.worldtools.option.general.capture.levelData": "Level.dat",
49 | "text.autoconfig.worldtools.option.general.capture.maps": "地圖",
50 | "text.autoconfig.worldtools.option.general.capture.metadata": "中繼資料",
51 | "text.autoconfig.worldtools.option.general.capture.players": "玩家",
52 | "text.autoconfig.worldtools.option.general.capture.statistics": "統計資料",
53 | "text.autoconfig.worldtools.option.general.compressLevel": "將擷取存檔為 zip 檔案",
54 | "text.autoconfig.worldtools.option.general.compressLevel.@Tooltip": "當擷取完成後,它將會被存檔為 zip 檔案。",
55 | "text.autoconfig.worldtools.option.render": "繪製",
56 | "text.autoconfig.worldtools.option.render.accentColor": "強調色(RGB 十六進位)",
57 | "text.autoconfig.worldtools.option.render.captureBarColor": "擷取資訊列顏色(RGB 十六進位)",
58 | "text.autoconfig.worldtools.option.render.captureBarStyle": "擷取資訊列樣式",
59 | "text.autoconfig.worldtools.option.render.unscannedContainerColor": "遺漏容器顏色(RGB 十六進位)",
60 | "text.autoconfig.worldtools.option.render.fromCacheLoadedContainerColor": "從快取載入的容器顏色 (RGB 十六進位)",
61 | "text.autoconfig.worldtools.option.render.unscannedEntityColor": "遺漏實體顏色(RGB 十六進位)",
62 | "text.autoconfig.worldtools.option.render.fromCacheLoadedEntityColor": "從快取載入的實體顏色(RGB 十六進位)",
63 | "text.autoconfig.worldtools.option.render.progressBarColor": "進度條顏色",
64 | "text.autoconfig.worldtools.option.render.progressBarStyle": "進度條樣式",
65 | "text.autoconfig.worldtools.option.render.progressBarTimeout": "進度條逾時(毫秒)",
66 | "text.autoconfig.worldtools.option.render.renderNotYetCachedContainers": "繪製尚未快取的容器",
67 | "text.autoconfig.worldtools.option.world.censor": "隱藏",
68 | "text.autoconfig.worldtools.option.world.gameRules": "遊戲規則",
69 | "text.autoconfig.worldtools.option.world.gameRules.doDaylightCycle": "晝夜更替",
70 | "text.autoconfig.worldtools.option.world.gameRules.doFireTick": "更新火焰",
71 | "text.autoconfig.worldtools.option.world.gameRules.doMobGriefing": "生物破壞",
72 | "text.autoconfig.worldtools.option.world.gameRules.doMobSpawning": "生物生成",
73 | "text.autoconfig.worldtools.option.world.gameRules.doPatrolSpawning": "巡邏隊生成",
74 | "text.autoconfig.worldtools.option.world.gameRules.doTraderSpawning": "流浪商人生成",
75 | "text.autoconfig.worldtools.option.world.gameRules.doVinesSpread": "藤蔓蔓延",
76 | "text.autoconfig.worldtools.option.world.gameRules.doWardenSpawning": "伏守者生成",
77 | "text.autoconfig.worldtools.option.world.gameRules.doWeatherCycle": "天氣循環",
78 | "text.autoconfig.worldtools.option.world.gameRules.keepInventory": "保留物品欄",
79 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules": "修改遊戲規則",
80 | "text.autoconfig.worldtools.option.world.gameRules.modifyGameRules.@Tooltip": "啟用後,遊戲規則將會被修改為此設定中設定的值。否則,遊戲規則將會保持不變。",
81 | "text.autoconfig.worldtools.option.world.metadata": "中繼資料",
82 | "text.autoconfig.worldtools.option.world.metadata.captureTimestamp": "儲存擷取的時間戳記",
83 | "text.autoconfig.worldtools.option.world.metadata.waterMark": "浮水印區塊",
84 | "text.autoconfig.worldtools.option.world.worldGenerator": "世界生成器",
85 | "text.autoconfig.worldtools.option.world.worldGenerator.bonusChest": "額外獎勵箱",
86 | "text.autoconfig.worldtools.option.world.worldGenerator.generateFeatures": "生成結構",
87 | "text.autoconfig.worldtools.option.world.worldGenerator.seed": "種子碼",
88 | "text.autoconfig.worldtools.option.world.worldGenerator.type": "世界生成類型",
89 | "text.autoconfig.worldtools.option.world.worldGenerator.type.@Tooltip": "VOID:將會生成一個虛空世界。FLAT:將會生成一個平坦的世界。DEFAULT:將會生成一個普通的世界。",
90 | "text.autoconfig.worldtools.title": "WorldTools 設定",
91 | "worldtools.capture.and": " 和 ",
92 | "worldtools.capture.chunk": "%s 個區塊",
93 | "worldtools.capture.chunks": "%s 個區塊",
94 | "worldtools.capture.click_to_open": "%s(按這裡開啟)",
95 | "worldtools.capture.container": "%s 個容器",
96 | "worldtools.capture.containers": "%s 個容器",
97 | "worldtools.capture.entities": "%s 個實體",
98 | "worldtools.capture.entity": "%s 個實體",
99 | "worldtools.capture.in_dimension": " 在維度 ",
100 | "worldtools.capture.loaded.block_entities": "從快取載入區塊 %s 在維度 %s 的方塊實體",
101 | "worldtools.capture.loaded.block_entities.anonymized": "從快取載入維度 %s 的方塊實體",
102 | "worldtools.capture.nothing_saved_yet": "%s 尚未儲存任何內容",
103 | "worldtools.capture.player": "%s 個玩家",
104 | "worldtools.capture.players": "%s 個玩家",
105 | "worldtools.capture.saved": "已儲存 ",
106 | "worldtools.capture.saved.advancements": "已儲存 %s 的進度",
107 | "worldtools.capture.saved.chunks": "已儲存維度 %s 中 %s 的區塊",
108 | "worldtools.capture.saved.chunks.anonymized": "已儲存維度 %s 中的區塊",
109 | "worldtools.capture.saved.compressed": "已將擷取的 ZIP 檔案儲存為 %s",
110 | "worldtools.capture.saved.end_flow": "已完成 %s 的擷取",
111 | "worldtools.capture.saved.entities": "已儲存維度 %s 中區塊 %s 的 %s",
112 | "worldtools.capture.saved.entities.anonymized": "已儲存維度 %s 中的 %s",
113 | "worldtools.capture.saved.levelData": "已儲存 %s 的 %s",
114 | "worldtools.capture.saved.mapData": "已儲存維度 %s 中的地圖",
115 | "worldtools.capture.saved.metadata": "已儲存 %s 的中繼資料",
116 | "worldtools.capture.saved.player": "已儲存維度 %s 中 %s 的玩家 %s",
117 | "worldtools.capture.saved.player.anonymized": "已儲存維度 %s 中的玩家 %s",
118 | "worldtools.capture.saved.statistics": "已儲存 %s 的統計資料",
119 | "worldtools.capture.to_directory": " 到儲存資料夾 ",
120 | "worldtools.capture.took": "(耗時 %s)",
121 | "worldtools.gui.browser.title": "WorldTools 瀏覽器",
122 | "worldtools.gui.capture.existing_world_confirm.message": "已存在名為「%s」的儲存檔案。要覆蓋它嗎?(將與現有資料合併)",
123 | "worldtools.gui.capture.existing_world_confirm.title": "找到現有世界",
124 | "worldtools.gui.escape.button.finish_download": "儲存 %s 的擷取",
125 | "worldtools.gui.manager.button.cancel": "取消",
126 | "worldtools.gui.manager.button.config": "設定",
127 | "worldtools.gui.manager.button.start_download": "開始下載",
128 | "worldtools.gui.manager.button.stop_download": "停止下載",
129 | "worldtools.gui.manager.title": "WorldTools 管理器",
130 | "worldtools.gui.manager.world_name_placeholder": "輸入世界名稱...(預設:%s)",
131 | "worldtools.key.categories": "WorldTools",
132 | "worldtools.key.open_config": "開啟設定",
133 | "worldtools.key.toggle_capture": "快速下載",
134 | "worldtools.log.error.already_capturing": "%s 的擷取已在執行!",
135 | "worldtools.log.error.failed_to_create_session": "無法為「%s」建立工作階段!(錯誤:%s)",
136 | "worldtools.log.error.failed_to_save_level": "無法儲存「%s」的 level.dat!(錯誤:%s)",
137 | "worldtools.log.error.failed_to_visit_file": "無法存取檔案 %s 以建立 zip 檔案!(錯誤:%s)",
138 | "worldtools.log.error.failed_to_zip": "無法建立工作階段以壓縮 %s 的擷取!(錯誤:%s)",
139 | "worldtools.log.error.not_capturing": "尚未執行任何擷取!",
140 | "worldtools.log.error.world_name_too_long": "世界名稱「%s」太長!(最多 %d 個字元)",
141 | "worldtools.log.info.singleplayer_capture": "實驗性:在單人遊戲中擷取是實驗性的,可能無法按預期運作!",
142 | "worldtools.log.info.started_capture": "開始擷取 %s...",
143 | "worldtools.log.info.stopping_capture": "正在停止擷取 %s..."
144 | }
--------------------------------------------------------------------------------
/common/src/main/resources/worldtools.accesswidener:
--------------------------------------------------------------------------------
1 | accessWidener v2 named
2 |
3 | accessible class net/minecraft/client/world/ClientChunkManager$ClientChunkMap
4 | accessible method net/minecraft/client/world/ClientChunkManager$ClientChunkMap isInRadius (II)Z
5 | accessible class net/minecraft/advancement/PlayerAdvancementTracker$ProgressMap
6 | accessible method net/minecraft/advancement/PlayerAdvancementTracker$ProgressMap (Ljava/util/Map;)V
7 | accessible field net/minecraft/client/network/ClientAdvancementManager advancementProgresses Ljava/util/Map;
8 | accessible class net/minecraft/stat/StatHandler
9 | accessible field net/minecraft/stat/StatHandler statMap Lit/unimi/dsi/fastutil/objects/Object2IntMap;
10 | accessible field net/minecraft/client/world/ClientChunkManager chunks Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;
11 | accessible method net/minecraft/client/world/ClientChunkManager$ClientChunkMap getChunk (I)Lnet/minecraft/world/chunk/WorldChunk;
12 | accessible field net/minecraft/client/world/ClientChunkManager$ClientChunkMap diameter I
13 | accessible method net/minecraft/client/world/ClientWorld getMapStates ()Ljava/util/Map;
14 | accessible field net/minecraft/entity/player/PlayerEntity enderChestInventory Lnet/minecraft/inventory/EnderChestInventory;
15 | accessible method net/minecraft/block/entity/LockableContainerBlockEntity getHeldStacks ()Lnet/minecraft/util/collection/DefaultedList;
16 | accessible method net/minecraft/block/entity/LockableContainerBlockEntity setHeldStacks (Lnet/minecraft/util/collection/DefaultedList;)V
17 | accessible method net/minecraft/world/chunk/SerializedChunk toNbt ([Lit/unimi/dsi/fastutil/shorts/ShortList;)Lnet/minecraft/nbt/NbtList;
--------------------------------------------------------------------------------
/common/src/main/resources/worldtools.mixins.common.json:
--------------------------------------------------------------------------------
1 | {
2 | "required": true,
3 | "minVersion": "0.8",
4 | "package": "org.waste.of.time.mixin",
5 | "compatibilityLevel": "JAVA_21",
6 | "client": [
7 | "BossBarHudMixin",
8 | "ClientPlayInteractionManagerMixin",
9 | "ClientPlayNetworkHandlerMixin",
10 | "ClientWorldMixin",
11 | "DebugRendererMixin",
12 | "GameMenuScreenMixin",
13 | "IntegratedServerLoaderMixin",
14 | "PalettedContainerMixin"
15 | ],
16 | "injectors": {
17 | "defaultRequire": 1
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/fabric/build.gradle.kts:
--------------------------------------------------------------------------------
1 | architectury {
2 | platformSetupLoomIde()
3 | fabric()
4 | }
5 |
6 | base.archivesName.set("${base.archivesName.get()}-fabric")
7 |
8 | loom {
9 | accessWidenerPath.set(project(":common").loom.accessWidenerPath)
10 | enableTransitiveAccessWideners.set(true)
11 | runs {
12 | getByName("client") {
13 | ideConfigGenerated(true)
14 | }
15 | }
16 | }
17 |
18 | val common: Configuration by configurations.creating {
19 | configurations.compileClasspath.get().extendsFrom(this)
20 | configurations.runtimeClasspath.get().extendsFrom(this)
21 | configurations["developmentFabric"].extendsFrom(this)
22 | }
23 |
24 | dependencies {
25 | common(project(":common", configuration = "namedElements")) { isTransitive = false }
26 | shadowCommon(project(path = ":common", configuration = "transformProductionFabric")) { isTransitive = false }
27 | modImplementation("net.fabricmc:fabric-loader:${project.properties["fabric_loader_version"]!!}")
28 | modImplementation("net.fabricmc.fabric-api:fabric-api:${project.properties["fabric_api_version"]!!}")
29 | modImplementation("net.fabricmc:fabric-language-kotlin:${project.properties["fabric_kotlin_version"]!!}")
30 | modApi("me.shedaniel.cloth:cloth-config-fabric:${project.properties["cloth_config_version"]}")
31 | modApi("com.terraformersmc:modmenu:${project.properties["mod_menu_version"]}")
32 | }
33 |
34 | tasks {
35 | processResources {
36 | inputs.property("version", project.version)
37 | filesMatching("fabric.mod.json") {
38 | expand(getProperties())
39 | expand(mutableMapOf("version" to project.version))
40 | }
41 | }
42 |
43 | remapJar {
44 | injectAccessWidener.set(true)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/fabric/gradle.properties:
--------------------------------------------------------------------------------
1 | loom.platform=fabric
2 |
--------------------------------------------------------------------------------
/fabric/src/main/java/org/waste/of/time/fabric/LoaderInfoImpl.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.fabric;
2 |
3 | import net.fabricmc.loader.api.FabricLoader;
4 |
5 | public class LoaderInfoImpl {
6 | public static String getVersion() {
7 | return FabricLoader.getInstance().getModContainer("worldtools").orElseThrow().getMetadata().getVersion().getFriendlyString();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/fabric/src/main/kotlin/org/waste/of/time/fabric/WorldToolsFabric.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.fabric
2 |
3 | import com.mojang.brigadier.CommandDispatcher
4 | import com.mojang.brigadier.arguments.StringArgumentType
5 | import com.mojang.brigadier.arguments.StringArgumentType.string
6 | import net.fabricmc.api.ClientModInitializer
7 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument
8 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal
9 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
10 | import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
11 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents
12 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents
13 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
14 | import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
15 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents
16 | import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
17 | import org.waste.of.time.Events
18 | import org.waste.of.time.WorldTools
19 | import org.waste.of.time.WorldTools.LOG
20 | import org.waste.of.time.manager.CaptureManager
21 |
22 | object WorldToolsFabric : ClientModInitializer {
23 | override fun onInitializeClient() {
24 | WorldTools.initialize()
25 |
26 | KeyBindingHelper.registerKeyBinding(WorldTools.CAPTURE_KEY)
27 | KeyBindingHelper.registerKeyBinding(WorldTools.CONFIG_KEY)
28 |
29 | ClientCommandRegistrationCallback.EVENT.register(ClientCommandRegistrationCallback { dispatcher, _ ->
30 | dispatcher.register()
31 | })
32 | ClientEntityEvents.ENTITY_LOAD.register(ClientEntityEvents.Load { entity, _ ->
33 | Events.onEntityLoad(entity)
34 | })
35 | ClientEntityEvents.ENTITY_UNLOAD.register(ClientEntityEvents.Unload { entity, _ ->
36 | Events.onEntityUnload(entity)
37 | })
38 | ClientPlayConnectionEvents.JOIN.register(ClientPlayConnectionEvents.Join { _, _, _ ->
39 | Events.onClientJoin()
40 | })
41 | ClientPlayConnectionEvents.DISCONNECT.register(ClientPlayConnectionEvents.Disconnect { _, _ ->
42 | Events.onClientDisconnect()
43 | })
44 | ClientTickEvents.START_CLIENT_TICK.register(ClientTickEvents.StartTick {
45 | Events.onClientTickStart()
46 | })
47 | ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { _, screen, _, _ ->
48 | ScreenEvents.remove(screen).register(ScreenEvents.Remove { screen ->
49 | Events.onScreenRemoved(screen)
50 | })
51 | })
52 | ClientChunkEvents.CHUNK_LOAD.register(ClientChunkEvents.Load { world, chunk ->
53 | Events.onChunkLoad(chunk)
54 | })
55 | ClientChunkEvents.CHUNK_UNLOAD.register(ClientChunkEvents.Unload { world, chunk ->
56 | if (chunk == null) return@Unload
57 | Events.onChunkUnload(chunk)
58 | })
59 |
60 | LOG.info("WorldTools Fabric initialized")
61 | }
62 |
63 | private fun CommandDispatcher.register() {
64 | register(
65 | literal("worldtools")
66 | .then(
67 | literal("capture")
68 | .then(
69 | literal("start")
70 | .executes { CaptureManager.start(); 0 }
71 | .then(
72 | argument("name", string())
73 | .executes {CaptureManager.start(it.getArgument("name", String::class.java)); 0 }
74 | )
75 | )
76 | .then(
77 | literal("stop")
78 | .executes { CaptureManager.stop(); 0 }
79 | )
80 | .executes { CaptureManager.toggleCapture(); 0 }
81 | )
82 | )
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/fabric/src/main/kotlin/org/waste/of/time/fabric/WorldToolsModMenuIntegration.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.fabric
2 |
3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory
4 | import com.terraformersmc.modmenu.api.ModMenuApi
5 | import me.shedaniel.autoconfig.AutoConfig
6 | import net.fabricmc.api.EnvType
7 | import net.fabricmc.api.Environment
8 | import net.minecraft.client.gui.screen.Screen
9 | import org.waste.of.time.config.WorldToolsConfig
10 |
11 | @Environment(EnvType.CLIENT)
12 | class WorldToolsModMenuIntegration : ModMenuApi {
13 |
14 | override fun getModConfigScreenFactory() =
15 | ConfigScreenFactory { parent: Screen? ->
16 | AutoConfig.getConfigScreen(WorldToolsConfig::class.java, parent).get()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/fabric/src/main/resources/fabric.mod.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": 1,
3 | "id": "worldtools",
4 | "version": "${version}",
5 | "name": "WorldTools",
6 | "description": "A powerful Minecraft mod that captures high-detail snapshots of server worlds locally.",
7 | "authors": [
8 | "Constructor",
9 | "P529",
10 | "rfresh"
11 | ],
12 | "contact": {
13 | "homepage": "https://github.com/Avanatiker/WorldTools/",
14 | "sources": "https://github.com/Avanatiker/WorldTools/"
15 | },
16 | "license": "GNU General Public License v3.0",
17 | "icon": "assets/worldtools/WorldTools.png",
18 | "environment": "*",
19 | "entrypoints": {
20 | "client": [
21 | {
22 | "adapter": "kotlin",
23 | "value": "org.waste.of.time.fabric.WorldToolsFabric"
24 | }
25 | ],
26 | "modmenu": [
27 | "org.waste.of.time.fabric.WorldToolsModMenuIntegration"
28 | ]
29 | },
30 | "mixins": [
31 | "worldtools.mixins.common.json",
32 | "worldtools.mixins.fabric.json"
33 | ],
34 | "depends": {
35 | "fabricloader": ">=${fabric_loader_version}",
36 | "fabric-api": "*",
37 | "minecraft": ["1.21.4"],
38 | "java": ">=21",
39 | "fabric-language-kotlin": ">=${fabric_kotlin_version}",
40 | "cloth-config": ">=15"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/fabric/src/main/resources/worldtools.mixins.fabric.json:
--------------------------------------------------------------------------------
1 | {
2 | "required": true,
3 | "minVersion": "0.8",
4 | "package": "org.waste.of.time.fabric.mixin",
5 | "compatibilityLevel": "JAVA_21",
6 | "client": [
7 | ],
8 | "injectors": {
9 | "defaultRequire": 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/forge/build.gradle.kts:
--------------------------------------------------------------------------------
1 | architectury {
2 | platformSetupLoomIde()
3 | forge()
4 | }
5 |
6 | base.archivesName.set("${base.archivesName.get()}-forge")
7 |
8 | loom {
9 | accessWidenerPath.set(project(":common").loom.accessWidenerPath)
10 | forge {
11 | convertAccessWideners = true
12 | extraAccessWideners.add(loom.accessWidenerPath.get().asFile.name)
13 | mixinConfig("worldtools.mixins.common.json")
14 | }
15 | }
16 |
17 | repositories {
18 | maven("https://thedarkcolour.github.io/KotlinForForge/") {
19 | name = "KotlinForForge"
20 | }
21 | maven("https://cursemaven.com") {
22 | name = "Curse"
23 | }
24 | }
25 |
26 | val common: Configuration by configurations.creating {
27 | configurations.compileClasspath.get().extendsFrom(this)
28 | configurations.runtimeClasspath.get().extendsFrom(this)
29 | configurations["developmentForge"].extendsFrom(this)
30 | }
31 |
32 | dependencies {
33 | forge("net.minecraftforge:forge:${project.properties["forge_version"]!!}")
34 | implementation("thedarkcolour:kotlinforforge:${project.properties["kotlin_forge_version"]!!}")
35 | common(project(":common", configuration = "namedElements")) { isTransitive = false }
36 | shadowCommon(project(path = ":common", configuration = "transformProductionForge")) { isTransitive = false }
37 | implementation(annotationProcessor("io.github.llamalad7:mixinextras-common:${project.properties["mixinextras_version"]}")!!)
38 | implementation(include("io.github.llamalad7:mixinextras-forge:${project.properties["mixinextras_version"]}")!!)
39 | modApi("me.shedaniel.cloth:cloth-config-forge:${project.properties["cloth_config_version"]}")
40 | }
41 |
42 | tasks {
43 | processResources {
44 | inputs.property("version", project.version)
45 |
46 | filesMatching("META-INF/mods.toml") {
47 | expand(getProperties())
48 | expand(mutableMapOf("version" to project.version))
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/forge/gradle.properties:
--------------------------------------------------------------------------------
1 | loom.platform=forge
2 |
--------------------------------------------------------------------------------
/forge/src/main/java/org/waste/of/time/forge/LoaderInfoImpl.java:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.forge;
2 |
3 | import net.minecraftforge.fml.loading.FMLLoader;
4 |
5 | public class LoaderInfoImpl {
6 | public static String getVersion() {
7 | return FMLLoader.getLoadingModList().getModFileById("worldtools").versionString();
8 | }
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/forge/src/main/kotlin/org/waste/of/time/forge/WorldToolsForge.kt:
--------------------------------------------------------------------------------
1 | package org.waste.of.time.forge
2 |
3 | import com.mojang.brigadier.CommandDispatcher
4 | import com.mojang.brigadier.arguments.StringArgumentType
5 | import net.minecraft.server.command.CommandManager.argument
6 | import net.minecraft.server.command.CommandManager.literal
7 | import net.minecraft.server.command.ServerCommandSource
8 | import net.minecraft.world.chunk.WorldChunk
9 | import net.minecraftforge.client.event.ClientPlayerNetworkEvent
10 | import net.minecraftforge.client.event.RegisterClientCommandsEvent
11 | import net.minecraftforge.client.event.RegisterKeyMappingsEvent
12 | import net.minecraftforge.client.event.ScreenEvent
13 | import net.minecraftforge.event.TickEvent
14 | import net.minecraftforge.event.TickEvent.ClientTickEvent
15 | import net.minecraftforge.event.entity.EntityJoinLevelEvent
16 | import net.minecraftforge.event.entity.EntityLeaveLevelEvent
17 | import net.minecraftforge.event.level.ChunkEvent
18 | import net.minecraftforge.fml.common.Mod
19 | import org.waste.of.time.Events
20 | import org.waste.of.time.WorldTools
21 | import org.waste.of.time.WorldTools.LOG
22 | import org.waste.of.time.manager.CaptureManager
23 | import thedarkcolour.kotlinforforge.forge.FORGE_BUS
24 |
25 | @Mod(WorldTools.MOD_ID)
26 | object WorldToolsForge {
27 | init {
28 | WorldTools.initialize()
29 | FORGE_BUS.addListener {
30 | it.register(WorldTools.CAPTURE_KEY)
31 | it.register(WorldTools.CONFIG_KEY)
32 | }
33 | FORGE_BUS.addListener {
34 | it.dispatcher.register()
35 | }
36 | FORGE_BUS.addListener {
37 | Events.onClientJoin()
38 | }
39 | FORGE_BUS.addListener {
40 | Events.onClientDisconnect()
41 | }
42 | FORGE_BUS.addListener {
43 | Events.onEntityLoad(it.entity)
44 | }
45 | FORGE_BUS.addListener {
46 | Events.onEntityUnload(it.entity)
47 | }
48 | FORGE_BUS.addListener {
49 | if (it.phase == TickEvent.Phase.START) Events.onClientTickStart()
50 | }
51 | FORGE_BUS.addListener {
52 | if (it.chunk is WorldChunk) Events.onChunkLoad(it.chunk as WorldChunk)
53 | }
54 | FORGE_BUS.addListener {
55 | if (it.chunk is WorldChunk) Events.onChunkUnload(it.chunk as WorldChunk)
56 | }
57 | FORGE_BUS.addListener {
58 | Events.onScreenRemoved(it.screen)
59 | }
60 |
61 | LOG.info("WorldTools Forge initialized")
62 | }
63 |
64 | private fun CommandDispatcher.register() {
65 | register(
66 | literal("worldtools")
67 | .then(literal("capture")
68 | .then(argument("name", StringArgumentType.string()).executes {
69 | CaptureManager.start(it.getArgument("name", String::class.java))
70 | 0
71 | })
72 | .then(literal("start").executes {
73 | CaptureManager.start()
74 | 0
75 | })
76 | .then(literal("stop").executes {
77 | CaptureManager.stop()
78 | 0
79 | })
80 | .executes {
81 | CaptureManager.toggleCapture()
82 | 0
83 | }
84 | )
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/forge/src/main/resources/META-INF/mods.toml:
--------------------------------------------------------------------------------
1 | modLoader = "kotlinforforge"
2 | loaderVersion = "[4,)"
3 | license = "GNU General Public License v3.0"
4 |
5 | [[mods]]
6 | modId = "worldtools"
7 | version = "${version}"
8 | displayName = "WorldTools"
9 | authors = "Constructor, P529, rfresh2"
10 | description = '''
11 | A powerful Minecraft mod that captures high-detail snapshots of server worlds locally.
12 | '''
13 | logoFile = "assets/worldtools/WorldTools.png"
14 | displayTest = "IGNORE_ALL_VERSION"
15 |
16 | [[dependencies.worldtools]]
17 | modId = "forge"
18 | mandatory = true
19 | versionRange = "[47,)"
20 | ordering = "NONE"
21 | side = "CLIENT"
22 |
23 | [[dependencies.worldtools]]
24 | modId = "minecraft"
25 | mandatory = true
26 | versionRange = "[1.21,)"
27 | ordering = "NONE"
28 | side = "CLIENT"
29 |
30 | [[dependencies.worldtools]]
31 | modId = "kotlinforforge"
32 | mandatory = true
33 | versionRange = "[4,)"
34 | ordering = "AFTER"
35 | side = "CLIENT"
36 |
37 | [[dependencies.worldtools]]
38 | modId = "cloth_config"
39 | mandatory = true
40 | versionRange = "[15,)"
41 | ordering = "AFTER"
42 | side = "CLIENT"
43 |
--------------------------------------------------------------------------------
/forge/src/main/resources/pack.mcmeta:
--------------------------------------------------------------------------------
1 | {
2 | "pack": {
3 | "description": "worldtools",
4 | "pack_format": 9
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | org.gradle.jvmargs=-Xmx2048M
3 |
4 | archives_base_name=WorldTools
5 | maven_group=org.waste.of.time
6 | mod_version=1.2.8
7 |
8 | minecraft_version=1.21.4
9 | enabled_platforms=fabric,forge
10 | architectury_version=15.0.1
11 | mixinextras_version=0.4.1
12 | cloth_config_version=17.0.144
13 | mod_menu_version=13.0.0
14 |
15 | # Fabric https://fabricmc.net/develop/
16 | fabric_loader_version=0.16.10
17 | yarn_mappings=1.21.4+build.8
18 | fabric_api_version=0.114.3+1.21.4
19 | fabric_kotlin_version=1.13.0+kotlin.2.1.0
20 |
21 | # Forge
22 | forge_version=1.21.4-54.0.17
23 | kotlin_forge_version=5.7.0
24 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avanatiker/WorldTools/f82b7415c5a5fab619cc8f537f36defdea09c546/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "WorldTools"
2 | pluginManagement {
3 | repositories {
4 | maven("https://maven.fabricmc.net/") {
5 | name = "Fabric"
6 | }
7 | maven("https://maven.architectury.dev/")
8 | maven("https://maven.minecraftforge.net/")
9 | mavenCentral()
10 | gradlePluginPortal()
11 | }
12 | }
13 |
14 | include("common")
15 | include("fabric")
16 | include("forge")
17 |
--------------------------------------------------------------------------------