@@ -5,130 +5,156 @@ import at.petrak.hexcasting.common.lib.hex.HexIotaTypes
55import com.google.gson.GsonBuilder
66import com.google.gson.JsonElement
77import com.google.gson.JsonObject
8+ import com.google.gson.JsonParseException
89import gay.`object`.hexdebug.HexDebug
9- import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererParser
10- import gay.`object`.hexdebug.api.client.splicing.SplicingTableIotaRendererProvider
1110import 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
1214import net.minecraft.resources.ResourceLocation
1315import net.minecraft.server.packs.resources.ResourceManager
1416import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
1517import 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
1723private 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+
2130object 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