Skip to content

Commit d604ef3

Browse files
authored
Use a tree structure for GTDynamicDataPack (#3109)
1 parent fc4f444 commit d604ef3

File tree

3 files changed

+175
-44
lines changed

3 files changed

+175
-44
lines changed

src/main/java/com/gregtechceu/gtceu/data/pack/GTDynamicDataPack.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.jetbrains.annotations.ApiStatus;
2626
import org.jetbrains.annotations.Nullable;
2727

28-
import java.io.ByteArrayInputStream;
2928
import java.io.IOException;
3029
import java.io.InputStream;
3130
import java.io.OutputStream;
@@ -41,7 +40,7 @@
4140
public class GTDynamicDataPack implements PackResources {
4241

4342
protected static final ObjectSet<String> SERVER_DOMAINS = new ObjectOpenHashSet<>();
44-
protected static final Map<ResourceLocation, byte[]> DATA = new HashMap<>();
43+
protected static final GTDynamicPackContents CONTENTS = new GTDynamicPackContents();
4544

4645
private final String name;
4746

@@ -59,7 +58,11 @@ public GTDynamicDataPack(String name, Collection<String> domains) {
5958
}
6059

6160
public static void clearServer() {
62-
DATA.clear();
61+
CONTENTS.clearData();
62+
}
63+
64+
private static void addToData(ResourceLocation location, byte[] bytes) {
65+
CONTENTS.addToData(location, bytes);
6366
}
6467

6568
public static void addRecipe(FinishedRecipe recipe) {
@@ -69,13 +72,13 @@ public static void addRecipe(FinishedRecipe recipe) {
6972
if (ConfigHolder.INSTANCE.dev.dumpRecipes) {
7073
writeJson(recipeId, "recipes", parent, recipeJson);
7174
}
72-
DATA.put(getRecipeLocation(recipeId), recipeJson.toString().getBytes(StandardCharsets.UTF_8));
75+
addToData(getRecipeLocation(recipeId), recipeJson.toString().getBytes(StandardCharsets.UTF_8));
7376
if (recipe.serializeAdvancement() != null) {
7477
JsonObject advancement = recipe.serializeAdvancement();
7578
if (ConfigHolder.INSTANCE.dev.dumpRecipes) {
7679
writeJson(recipe.getAdvancementId(), "advancements", parent, advancement);
7780
}
78-
DATA.put(getAdvancementLocation(Objects.requireNonNull(recipe.getAdvancementId())),
81+
addToData(getAdvancementLocation(Objects.requireNonNull(recipe.getAdvancementId())),
7982
advancement.toString().getBytes(StandardCharsets.UTF_8));
8083
}
8184
}
@@ -109,9 +112,7 @@ public static void writeJson(ResourceLocation id, @Nullable String subdir, Path
109112

110113
public static void addAdvancement(ResourceLocation loc, JsonObject obj) {
111114
ResourceLocation l = getAdvancementLocation(loc);
112-
synchronized (DATA) {
113-
DATA.put(l, obj.toString().getBytes(StandardCharsets.UTF_8));
114-
}
115+
addToData(l, obj.toString().getBytes(StandardCharsets.UTF_8));
115116
}
116117

117118
@Nullable
@@ -123,10 +124,7 @@ public IoSupplier<InputStream> getRootResource(String... elements) {
123124
@Override
124125
public IoSupplier<InputStream> getResource(PackType type, ResourceLocation location) {
125126
if (type == PackType.SERVER_DATA) {
126-
var byteArray = DATA.get(location);
127-
if (byteArray != null)
128-
return () -> new ByteArrayInputStream(byteArray);
129-
else return null;
127+
return CONTENTS.getResource(location);
130128
} else {
131129
return null;
132130
}
@@ -135,15 +133,7 @@ public IoSupplier<InputStream> getResource(PackType type, ResourceLocation locat
135133
@Override
136134
public void listResources(PackType packType, String namespace, String path, ResourceOutput resourceOutput) {
137135
if (packType == PackType.SERVER_DATA) {
138-
if (!path.endsWith("/")) path += "/";
139-
final String finalPath = path;
140-
DATA.keySet().stream().filter(Objects::nonNull).filter(loc -> loc.getPath().startsWith(finalPath))
141-
.forEach((id) -> {
142-
IoSupplier<InputStream> resource = this.getResource(packType, id);
143-
if (resource != null) {
144-
resourceOutput.accept(id, resource);
145-
}
146-
});
136+
CONTENTS.listResources(namespace, path, resourceOutput);
147137
}
148138
}
149139

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.gregtechceu.gtceu.data.pack;
2+
3+
import net.minecraft.resources.ResourceLocation;
4+
import net.minecraft.server.packs.PackResources;
5+
import net.minecraft.server.packs.resources.IoSupplier;
6+
7+
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
import java.io.ByteArrayInputStream;
11+
import java.io.InputStream;
12+
import java.util.Map;
13+
import java.util.concurrent.locks.ReadWriteLock;
14+
import java.util.concurrent.locks.ReentrantReadWriteLock;
15+
16+
/**
17+
* Stores contents of a dynamic resource pack in a tree-style format for efficient traversal. This class can safely
18+
* be accessed from multiple threads; it implements synchronization internally.
19+
*
20+
* @author embeddedt
21+
*/
22+
public class GTDynamicPackContents {
23+
24+
private static class Node {
25+
26+
/**
27+
* Holds either a IoSupplier<InputStream> with the data for a given location, or a map of string -> Node.
28+
*/
29+
Object contents = new Object2ObjectOpenHashMap<>();
30+
31+
void collectResources(String namespace, String[] pathComponents, int curIndex,
32+
PackResources.ResourceOutput output) {
33+
if (curIndex < pathComponents.length) {
34+
String component = pathComponents[curIndex];
35+
36+
Node n = getChild(component);
37+
if (n != null) {
38+
n.collectResources(namespace, pathComponents, curIndex + 1, output);
39+
}
40+
} else {
41+
// We reached the desired path. Collect all resources
42+
this.outputResources(namespace, String.join("/", pathComponents), output);
43+
}
44+
}
45+
46+
private boolean isTerminalNode() {
47+
return contents instanceof IoSupplier<?>;
48+
}
49+
50+
@SuppressWarnings("unchecked")
51+
private Map<String, Node> getChildren() {
52+
if (!(contents instanceof Map<?, ?>)) {
53+
throw new IllegalStateException("attempting to get children on a terminal node");
54+
}
55+
return (Map<String, Node>) contents;
56+
}
57+
58+
void outputResources(String namespace, String path, PackResources.ResourceOutput output) {
59+
if (isTerminalNode()) {
60+
// This is a terminal node.
61+
ResourceLocation location = new ResourceLocation(namespace, path);
62+
output.accept(location, this.createIoSupplier());
63+
} else {
64+
for (var entry : getChildren().entrySet()) {
65+
entry.getValue().outputResources(namespace, path + "/" + entry.getKey(), output);
66+
}
67+
}
68+
}
69+
70+
@SuppressWarnings("unchecked")
71+
IoSupplier<InputStream> createIoSupplier() {
72+
if (!isTerminalNode()) {
73+
throw new IllegalStateException("Node has no data");
74+
}
75+
// Capture the byte array here to avoid capturing the whole node in the lambda
76+
return (IoSupplier<InputStream>) contents;
77+
}
78+
79+
@Nullable
80+
Node getChild(String name) {
81+
if (isTerminalNode()) {
82+
return null;
83+
} else {
84+
return getChildren().get(name);
85+
}
86+
}
87+
}
88+
89+
private final Node root = new Node();
90+
private final ReadWriteLock lock = new ReentrantReadWriteLock();
91+
92+
public void addToData(ResourceLocation location, byte[] bytes) {
93+
addToData(location, () -> new ByteArrayInputStream(bytes));
94+
}
95+
96+
public void addToData(ResourceLocation location, IoSupplier<InputStream> supplier) {
97+
String[] pathComponents = location.getPath().split("/");
98+
var lock = this.lock.writeLock();
99+
lock.lock();
100+
try {
101+
Node node = root.getChildren().computeIfAbsent(location.getNamespace(), $ -> new Node());
102+
for (String component : pathComponents) {
103+
node = node.getChildren().computeIfAbsent(component, $ -> new Node());
104+
}
105+
node.contents = supplier;
106+
} finally {
107+
lock.unlock();
108+
}
109+
}
110+
111+
public void clearData() {
112+
var lock = this.lock.writeLock();
113+
lock.lock();
114+
try {
115+
root.getChildren().clear();
116+
} finally {
117+
lock.unlock();
118+
}
119+
}
120+
121+
public IoSupplier<InputStream> getResource(ResourceLocation location) {
122+
var lock = this.lock.readLock();
123+
lock.lock();
124+
try {
125+
Node node = this.root.getChild(location.getNamespace());
126+
String[] pathComponents = location.getPath().split("/");
127+
for (String path : pathComponents) {
128+
if (node == null) {
129+
return null;
130+
}
131+
node = node.getChild(path);
132+
}
133+
if (node == null) {
134+
return null;
135+
}
136+
return node.createIoSupplier();
137+
} finally {
138+
lock.unlock();
139+
}
140+
}
141+
142+
public void listResources(String namespace, String path, PackResources.ResourceOutput resourceOutput) {
143+
var lock = this.lock.readLock();
144+
lock.lock();
145+
try {
146+
Node base = this.root.getChild(namespace);
147+
if (base == null) {
148+
return;
149+
}
150+
base.collectResources(namespace, path.split("/"), 0, resourceOutput);
151+
} finally {
152+
lock.unlock();
153+
}
154+
}
155+
}

src/main/java/com/gregtechceu/gtceu/data/pack/GTDynamicResourcePack.java

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,14 @@
2222
import org.jetbrains.annotations.ApiStatus;
2323
import org.jetbrains.annotations.Nullable;
2424

25-
import java.io.ByteArrayInputStream;
2625
import java.io.IOException;
2726
import java.io.InputStream;
2827
import java.io.OutputStream;
2928
import java.nio.charset.StandardCharsets;
3029
import java.nio.file.Files;
3130
import java.nio.file.Path;
3231
import java.util.Collection;
33-
import java.util.Objects;
3432
import java.util.Set;
35-
import java.util.concurrent.ConcurrentHashMap;
36-
import java.util.concurrent.ConcurrentMap;
3733
import java.util.function.Supplier;
3834
import java.util.stream.Collectors;
3935

@@ -46,8 +42,7 @@
4642
public class GTDynamicResourcePack implements PackResources {
4743

4844
protected static final ObjectSet<String> CLIENT_DOMAINS = new ObjectOpenHashSet<>();
49-
@ApiStatus.Internal
50-
public static final ConcurrentMap<ResourceLocation, byte[]> DATA = new ConcurrentHashMap<>();
45+
protected static final GTDynamicPackContents CONTENTS = new GTDynamicPackContents();
5146

5247
private final String name;
5348

@@ -65,7 +60,7 @@ public GTDynamicResourcePack(String name, Collection<String> domains) {
6560
}
6661

6762
public static void clearClient() {
68-
DATA.clear();
63+
CONTENTS.clearData();
6964
}
7065

7166
public static void addBlockModel(ResourceLocation loc, JsonElement obj) {
@@ -74,7 +69,7 @@ public static void addBlockModel(ResourceLocation loc, JsonElement obj) {
7469
Path parent = GTCEu.getGameDir().resolve("gtceu/dumped/assets");
7570
writeJson(l, null, parent, obj);
7671
}
77-
DATA.put(l, obj.toString().getBytes(StandardCharsets.UTF_8));
72+
CONTENTS.addToData(l, obj.toString().getBytes(StandardCharsets.UTF_8));
7873
}
7974

8075
public static void addBlockModel(ResourceLocation loc, Supplier<JsonElement> obj) {
@@ -87,7 +82,7 @@ public static void addItemModel(ResourceLocation loc, JsonElement obj) {
8782
Path parent = GTCEu.getGameDir().resolve("gtceu/dumped/assets");
8883
writeJson(l, null, parent, obj);
8984
}
90-
DATA.put(l, obj.toString().getBytes(StandardCharsets.UTF_8));
85+
CONTENTS.addToData(l, obj.toString().getBytes(StandardCharsets.UTF_8));
9186
}
9287

9388
public static void addItemModel(ResourceLocation loc, Supplier<JsonElement> obj) {
@@ -100,7 +95,7 @@ public static void addBlockState(ResourceLocation loc, JsonElement stateJson) {
10095
Path parent = GTCEu.getGameDir().resolve("gtceu/dumped/assets");
10196
writeJson(l, null, parent, stateJson);
10297
}
103-
DATA.put(l, stateJson.toString().getBytes(StandardCharsets.UTF_8));
98+
CONTENTS.addToData(l, stateJson.toString().getBytes(StandardCharsets.UTF_8));
10499
}
105100

106101
public static void addBlockState(ResourceLocation loc, Supplier<JsonElement> generator) {
@@ -113,7 +108,7 @@ public static void addBlockTexture(ResourceLocation loc, byte[] data) {
113108
Path parent = GTCEu.getGameDir().resolve("gtceu/dumped/assets");
114109
writeByteArray(l, null, parent, data);
115110
}
116-
DATA.put(l, data);
111+
CONTENTS.addToData(l, data);
117112
}
118113

119114
public static void addItemTexture(ResourceLocation loc, byte[] data) {
@@ -122,7 +117,7 @@ public static void addItemTexture(ResourceLocation loc, byte[] data) {
122117
Path parent = GTCEu.getGameDir().resolve("gtceu/dumped/assets");
123118
writeByteArray(l, null, parent, data);
124119
}
125-
DATA.put(l, data);
120+
CONTENTS.addToData(l, data);
126121
}
127122

128123
@ApiStatus.Internal
@@ -153,24 +148,15 @@ public IoSupplier<InputStream> getRootResource(String... elements) {
153148
@Override
154149
public IoSupplier<InputStream> getResource(PackType type, ResourceLocation location) {
155150
if (type == PackType.CLIENT_RESOURCES) {
156-
if (DATA.containsKey(location))
157-
return () -> new ByteArrayInputStream(DATA.get(location));
151+
return CONTENTS.getResource(location);
158152
}
159153
return null;
160154
}
161155

162156
@Override
163157
public void listResources(PackType packType, String namespace, String path, ResourceOutput resourceOutput) {
164158
if (packType == PackType.CLIENT_RESOURCES) {
165-
if (!path.endsWith("/")) path += "/";
166-
final String finalPath = path;
167-
DATA.keySet().stream().filter(Objects::nonNull).filter(loc -> loc.getPath().startsWith(finalPath))
168-
.forEach((id) -> {
169-
IoSupplier<InputStream> resource = this.getResource(packType, id);
170-
if (resource != null) {
171-
resourceOutput.accept(id, resource);
172-
}
173-
});
159+
CONTENTS.listResources(namespace, path, resourceOutput);
174160
}
175161
}
176162

0 commit comments

Comments
 (0)