Skip to content

Commit 36a9108

Browse files
committed
Add support for nested iota providers using DFS topological sorter
1 parent 8d1d724 commit 36a9108

File tree

5 files changed

+143
-97
lines changed

5 files changed

+143
-97
lines changed

Common/src/main/java/gay/object/hexdebug/api/client/splicing/SplicingTableIotaRendererParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import com.google.gson.Gson;
44
import com.google.gson.JsonObject;
5-
import com.mojang.datafixers.util.Function3;
6-
import com.mojang.datafixers.util.Function4;
5+
import org.jetbrains.annotations.ApiStatus;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
98

@@ -14,6 +13,7 @@ public interface SplicingTableIotaRendererParser<T extends SplicingTableIotaRend
1413
* Throws {@link IllegalArgumentException} or {@link com.google.gson.JsonParseException} if
1514
* the input is invalid.
1615
*/
16+
@ApiStatus.OverrideOnly
1717
@NotNull
1818
T parse(@NotNull Gson gson, @NotNull JsonObject jsonObject, @Nullable T parent);
1919

Common/src/main/java/gay/object/hexdebug/api/client/splicing/SplicingTableIotaRendererProvider.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package gay.object.hexdebug.api.client.splicing;
22

33
import at.petrak.hexcasting.api.casting.iota.IotaType;
4-
import at.petrak.hexcasting.common.lib.hex.HexIotaTypes;
54
import gay.object.hexdebug.api.splicing.SplicingTableIotaClientView;
6-
import gay.object.hexdebug.gui.splicing.SplicingTableScreen;
7-
import net.minecraft.client.gui.components.Tooltip;
8-
import net.minecraft.network.chat.Component;
95
import org.jetbrains.annotations.NotNull;
106
import org.jetbrains.annotations.Nullable;
117

Common/src/main/java/gay/object/hexdebug/api/client/splicing/SplicingTableIotaRenderers.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package gay.object.hexdebug.api.client.splicing;
22

33
import com.google.common.collect.Maps;
4+
import com.google.gson.JsonObject;
45
import gay.object.hexdebug.HexDebug;
6+
import gay.object.hexdebug.resources.splicing.SplicingTableIotasResourceReloadListener;
57
import net.minecraft.resources.ResourceLocation;
68
import org.jetbrains.annotations.ApiStatus;
79
import org.jetbrains.annotations.NotNull;
@@ -18,18 +20,40 @@ public final class SplicingTableIotaRenderers {
1820
* This is used to parse resource files and create providers for iota renderers.
1921
*/
2022
public static void register(
21-
@NotNull ResourceLocation id,
23+
@NotNull ResourceLocation parserId,
2224
@NotNull SplicingTableIotaRendererParser<?> parser
2325
) {
24-
if (PARSERS.containsKey(id)) {
25-
HexDebug.LOGGER.warn("Overriding existing splicing table iota renderer parser: {}", id);
26+
if (PARSERS.containsKey(parserId)) {
27+
HexDebug.LOGGER.warn("Overriding existing splicing table iota renderer parser: {}", parserId);
2628
}
27-
PARSERS.put(id, parser);
29+
PARSERS.put(parserId, parser);
30+
}
31+
32+
/**
33+
* Get/load a provider by ID. Throws if the referenced provider does not exist or fails to load.
34+
* Intended for use in providers that reference other providers.
35+
* <br>
36+
* This should only be called from inside of {@link SplicingTableIotaRendererParser#parse}.
37+
*/
38+
@NotNull
39+
public static SplicingTableIotaRendererProvider loadProvider(@NotNull ResourceLocation providerId) {
40+
return SplicingTableIotasResourceReloadListener.loadProvider(providerId);
41+
}
42+
43+
/**
44+
* Parse a provider from a raw JSON object. Intended for use in providers that contain other
45+
* providers.
46+
* <br>
47+
* This should only be called from inside of {@link SplicingTableIotaRendererParser#parse}.
48+
*/
49+
@NotNull
50+
public static SplicingTableIotaRendererProvider parseProvider(@NotNull JsonObject jsonObject) {
51+
return SplicingTableIotasResourceReloadListener.parseProvider(jsonObject);
2852
}
2953

3054
@ApiStatus.Internal
3155
@Nullable
32-
public static SplicingTableIotaRendererParser<?> getParser(@NotNull ResourceLocation id) {
33-
return PARSERS.get(id);
56+
public static SplicingTableIotaRendererParser<?> getParser(@NotNull ResourceLocation parserId) {
57+
return PARSERS.get(parserId);
3458
}
3559
}

Common/src/main/kotlin/gay/object/hexdebug/gui/splicing/widgets/BaseIotaButton.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ abstract class BaseIotaButton(x: Int, y: Int) : HexagonButton(
6161

6262
try {
6363
renderer = iotaType
64-
?.let { SplicingTableIotasResourceReloadListener.PROVIDERS[it] }
64+
?.let { SplicingTableIotasResourceReloadListener.getProvider(it) }
6565
?.createRenderer(iotaType, iotaView, x, y)
66-
?: SplicingTableIotasResourceReloadListener.FALLBACK
66+
?: SplicingTableIotasResourceReloadListener.fallback
6767
?.createRenderer(iotaType, iotaView, x, y)
6868

6969
tooltip = renderer?.createTooltip()

Common/src/main/kotlin/gay/object/hexdebug/resources/splicing/SplicingTableIotasResourceReloadListener.kt

Lines changed: 109 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,130 +5,156 @@ import at.petrak.hexcasting.common.lib.hex.HexIotaTypes
55
import com.google.gson.GsonBuilder
66
import com.google.gson.JsonElement
77
import com.google.gson.JsonObject
8+
import com.google.gson.JsonParseException
89
import gay.`object`.hexdebug.HexDebug
9-
import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererParser
10-
import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererProvider
1110
import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRenderers
11+
import gay.`object`.hexdebug.utils.contains
12+
import gay.`object`.hexdebug.utils.getAsResourceLocation
13+
import gay.`object`.hexdebug.utils.getOrNull
1214
import net.minecraft.resources.ResourceLocation
1315
import net.minecraft.server.packs.resources.ResourceManager
1416
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
1517
import net.minecraft.util.profiling.ProfilerFiller
18+
import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererParser as RendererParser
19+
import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererProvider as RendererProvider
20+
21+
private typealias AnyRendererParser = RendererParser<RendererProvider>
1622

1723
private val GSON = GsonBuilder()
1824
.registerTypeAdapter(ResourceLocation::class.java, ResourceLocation.Serializer())
1925
.create()
2026

27+
// this is effectively a topological sorting algorithm using depth-first search
28+
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
29+
2130
object SplicingTableIotasResourceReloadListener :
2231
SimpleJsonResourceReloadListener(GSON, "hexdebug_splicing_iotas")
2332
{
24-
val PROVIDERS = mutableMapOf<IotaType<*>, SplicingTableIotaRendererProvider>()
25-
var FALLBACK: SplicingTableIotaRendererProvider? = null
33+
var fallback: RendererProvider? = null
34+
private set
35+
36+
private val providersByType = mutableMapOf<IotaType<*>, RendererProvider>()
37+
38+
private val objects = mutableMapOf<ResourceLocation, JsonObject>()
39+
private val parsersByProviderId = mutableMapOf<ResourceLocation, AnyRendererParser>()
40+
private val providersById = mutableMapOf<ResourceLocation, RendererProvider>()
41+
private val visitingProviders = linkedSetOf<ResourceLocation>() // use a linked set so the error message is ordered
42+
private val failedProviders = mutableSetOf<ResourceLocation>()
43+
44+
fun getProvider(iotaType: IotaType<*>): RendererProvider? = providersByType[iotaType]
45+
46+
@JvmStatic
47+
fun loadProvider(providerId: ResourceLocation): RendererProvider {
48+
check(visitingProviders.isNotEmpty()) {
49+
"Tried to call loadProvider outside of SplicingTableIotaRendererParser#parse"
50+
}
51+
val jsonObject = objects[providerId]
52+
?: throw IllegalArgumentException("Provider $providerId not found")
53+
return visit(providerId, jsonObject)
54+
}
55+
56+
@JvmStatic
57+
fun parseProvider(jsonObject: JsonObject): RendererProvider {
58+
check(visitingProviders.isNotEmpty()) {
59+
"Tried to call parseProvider outside of SplicingTableIotaRendererParser#parse"
60+
}
61+
return visit(jsonObject).second
62+
}
2663

2764
override fun apply(
2865
map: MutableMap<ResourceLocation, JsonElement>,
2966
manager: ResourceManager,
3067
profiler: ProfilerFiller
3168
) {
3269
HexDebug.LOGGER.info("Loading splicing table iota renderers...")
33-
PROVIDERS.clear()
34-
FALLBACK = null
3570

36-
// first, filter out any weird non-object files
37-
val objects = mutableMapOf<ResourceLocation, JsonObject>()
71+
fallback = null
72+
providersByType.clear()
73+
74+
objects.clear()
75+
parsersByProviderId.clear()
76+
providersById.clear()
77+
visitingProviders.clear()
78+
failedProviders.clear()
79+
80+
// filter out any weird non-object files
3881
for ((id, jsonElement) in map) {
3982
if (!jsonElement.isJsonObject) continue
4083
objects[id] = jsonElement.asJsonObject
4184
}
4285

43-
// next, find a topological order to load the providers
44-
val stack = mutableListOf<ResourceLocation>()
45-
val parsers = mutableMapOf<ResourceLocation, SplicingTableIotaRendererParser<SplicingTableIotaRendererProvider>>()
46-
for (id in objects.keys) {
47-
findParents(objects, id)?.let { (toAdd, parser) ->
48-
for (it in toAdd) {
49-
stack.add(it)
50-
@Suppress("UNCHECKED_CAST")
51-
parsers[it] = parser as SplicingTableIotaRendererParser<SplicingTableIotaRendererProvider>
52-
}
86+
// visit all providers
87+
for ((id, jsonObject) in objects) {
88+
if (id in failedProviders) continue
89+
try {
90+
visit(id, jsonObject)
91+
} catch (e: Exception) {
92+
HexDebug.LOGGER.error("Caught exception while loading renderer for $id, skipping", e)
93+
failedProviders.add(id)
94+
failedProviders.addAll(visitingProviders)
95+
visitingProviders.clear()
5396
}
5497
}
5598

56-
// finally, actually load the providers
57-
val providers = mutableMapOf<ResourceLocation, SplicingTableIotaRendererProvider>()
58-
val failed = mutableSetOf<ResourceLocation>()
59-
for (id in stack.asReversed()) {
60-
if (id in providers || id in failed) continue
61-
62-
val provider = try {
63-
val parser = parsers[id]!!
64-
val jsonObject = objects[id]!!
65-
val parent = if (jsonObject.has("parent")) {
66-
val parentId = ResourceLocation(jsonObject.getAsJsonPrimitive("parent").asString)
67-
// we're loading parents first, so this should always be present unless the parent failed
68-
providers[parentId] ?: continue
69-
} else null
70-
parser.parse(GSON, jsonObject, parent)
71-
} catch (e: Exception) {
72-
HexDebug.LOGGER.error("Caught exception while parsing $id, ignoring", e)
73-
failed.add(id)
74-
continue
75-
}
99+
HexDebug.LOGGER.info("Loaded ${providersById.size} splicing table iota renderers for ${providersByType.size} iota types")
100+
}
76101

77-
providers[id] = provider
78-
if (HexIotaTypes.REGISTRY.containsKey(id)) {
79-
PROVIDERS[HexIotaTypes.REGISTRY.get(id)!!] = provider
80-
}
81-
if (id == HexDebug.id("builtin/generic")) {
82-
FALLBACK = provider
83-
}
102+
private fun visit(providerId: ResourceLocation, jsonObject: JsonObject): RendererProvider {
103+
providersById[providerId]?.let { return it }
104+
105+
if (!visitingProviders.add(providerId)) {
106+
throw IllegalStateException("Cycle detected: ${visitingProviders.joinToString()}, $providerId")
107+
}
108+
109+
val (parser, provider) = try {
110+
visit(jsonObject)
111+
} catch (e: ProviderVisitException) {
112+
throw e // don't make a huge unnecessary stack trace
113+
} catch (e: Exception) {
114+
throw ProviderVisitException("Failed to parse provider $providerId", e)
115+
}
116+
117+
visitingProviders.remove(providerId)
118+
119+
parsersByProviderId[providerId] = parser
120+
providersById[providerId] = provider
121+
HexIotaTypes.REGISTRY.getOrNull(providerId)?.let {
122+
providersByType[it] = provider
123+
}
124+
if (providerId == HexDebug.id("builtin/generic")) {
125+
fallback = provider
84126
}
85127

86-
HexDebug.LOGGER.info("Loaded ${providers.size} splicing table iota renderers for ${PROVIDERS.size} iota types")
128+
return provider
87129
}
88130

89-
private fun findParents(
90-
objects: Map<ResourceLocation, JsonObject>,
91-
id: ResourceLocation,
92-
): Pair<List<ResourceLocation>, SplicingTableIotaRendererParser<*>>? {
93-
var jsonObject = objects[id]!!
94-
val knownIds = mutableSetOf(id)
95-
val stack = mutableListOf(id)
96-
97-
while (jsonObject.has("parent")) {
98-
val parentId = try {
99-
ResourceLocation(jsonObject.getAsJsonPrimitive("parent").asString)
100-
} catch (e: Exception) {
101-
HexDebug.LOGGER.error("Failed to parse parent field of ${stack.last()} while loading $id, ignoring", e)
102-
return null
103-
}
131+
private fun visit(jsonObject: JsonObject): Pair<AnyRendererParser, RendererProvider> {
132+
val (parser, parent) = when {
133+
"parent" in jsonObject -> {
134+
val parentId = jsonObject.getAsResourceLocation("parent")
135+
val parentObject = objects[parentId]
136+
?: throw JsonParseException("Parent $parentId not found")
104137

105-
if (!knownIds.add(parentId)) {
106-
HexDebug.LOGGER.error("Loop found in parents of $id ($parentId appears twice), ignoring")
107-
return null
108-
}
138+
// DFS recursion - ensure the parent has been loaded first
139+
val provider = visit(parentId, parentObject)
109140

110-
if (parentId !in objects) {
111-
HexDebug.LOGGER.error("Parent id $parentId not found while loading $id, ignoring")
112-
return null
141+
parsersByProviderId[parentId]!! to provider
113142
}
114143

115-
stack.add(parentId)
116-
jsonObject = objects[parentId]!!
117-
}
118-
119-
val typeId = try {
120-
ResourceLocation(jsonObject.getAsJsonPrimitive("type").asString)
121-
} catch (e: Exception) {
122-
HexDebug.LOGGER.error("Failed to parse type field of ${stack.last()} while loading $id, ignoring", e)
123-
return null
124-
}
144+
"type" in jsonObject -> {
145+
val typeId = jsonObject.getAsResourceLocation("type")
146+
val parser = SplicingTableIotaRenderers.getParser(typeId)
147+
?: throw JsonParseException("Parser type $typeId not found")
148+
@Suppress("UNCHECKED_CAST") // shhhhhhh
149+
parser as AnyRendererParser to null
150+
}
125151

126-
val parser = SplicingTableIotaRenderers.getParser(typeId)
127-
if (parser == null) {
128-
HexDebug.LOGGER.error("Unrecognized type $typeId for ${stack.last()} while loading $id, ignoring")
129-
return null
152+
else -> throw JsonParseException("Expected parent or type field but got neither")
130153
}
131154

132-
return stack to parser
155+
// there may be more recursion here if the parser calls loadProvider or parseProvider
156+
return parser to parser.parse(GSON, jsonObject, parent)
133157
}
134158
}
159+
160+
class ProviderVisitException(message: String? = null, cause: Throwable? = null) : IllegalStateException(message, cause)

0 commit comments

Comments
 (0)