Skip to content

Commit 4130d01

Browse files
committed
Merge branch 'chore/archie-reorg-refactor-docs' into 1.21.x
2 parents ad8a8a1 + afc97bc commit 4130d01

19 files changed

Lines changed: 253 additions & 118 deletions

File tree

AGENTS.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# AGENTS Guide for Archie
2+
3+
## Repository shape
4+
- Multi-module Architectury mod: `common` (shared API/logic), `fabric`, `neoforge` (`settings.gradle.kts`).
5+
- `common` is the source of truth; loader modules mostly provide bootstrapping, loader deps, and `actual` implementations.
6+
- Main entrypoint flow is `Archie.init()` -> register events/network/config/datagen/gametest gates (`common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt`).
7+
8+
## Architecture patterns to preserve
9+
- Cross-loader abstractions use Kotlin `expect/actual` files named `*.common.kt`, `*.fabric.kt`, `*.neoforge.kt` (example: `APlatform`, `ADataGeneratorPlatform`, `AGameTestPlatform`).
10+
- Loader entrypoints must only delegate into common init methods:
11+
- Fabric: `ArchieFabric.onInitialize*` (`fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt`)
12+
- NeoForge: bus listeners in `ArchieNeoForge` (`neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt`)
13+
- Networking is centralized via `NetworkChannel`; packets must be `@Serializable data class` and registered before `register()` (`common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt`).
14+
- `ArchieNetworkChannel.init()` is the canonical registration order example (register packet producers/consumers, then call `register()`).
15+
16+
## Build and run workflows
17+
- Build all modules + merged artifact: `./gradlew build` (root `build`/`assemble` finalize with `fusejars` in `build.gradle.kts`).
18+
- Loader-specific dev runs: `./gradlew fabric:runClient`, `./gradlew neoforge:runClient`.
19+
- Datagen runs are explicit tasks: `./gradlew fabric:runDatagen` / `./gradlew neoforge:runDatagen`.
20+
- GameTest runs: `./gradlew fabric:runGametest` / `./gradlew neoforge:runGametest`.
21+
- Docs pipeline: `embedDokkaIntoMkDocs` then `publishDocs` (calls `mike deploy ...`); `mkdocs.yml` contains `# !!! EMBEDDED DOKKA ... DO NOT COMMIT !!!` markers.
22+
23+
## Project-specific conventions
24+
- Keep resource/manifests tokenized using Gradle properties (`${mod_id}`, `${versions.*}`) in `fabric.mod.json` and `neoforge.mods.toml`.
25+
- Shared assets are merged from `common` into loader modules via `processResources`; do not duplicate `assets/archie/**` directly in loader modules unless loader-specific.
26+
- `common/build.gradle.kts` intentionally uses `modImplementation(libs.fabric.loader)` only for annotations/mixin deps; avoid importing random Fabric-only classes in common code.
27+
- Utility operators are used pervasively for IDs (`Archie % "main"`, `"namespace" % "path"`) from `common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt`.
28+
29+
## Dependency and integration touchpoints
30+
- Versions and plugin IDs are centralized in `gradle/libs.versions.toml`; update there first.
31+
- Packaging/publishing is configured at root via `modfusioner` (`fusejars`) and `modpublisher` (CurseForge/Modrinth IDs and required deps) in `build.gradle.kts`.
32+
- Mixins are split by scope: loader mixins in `fabric/src/main/resources/archie.mixins.json` and `neoforge/src/main/resources/archie.mixins.json`, common mixin config in `common/src/main/resources/archie-common.mixins.json`.
33+
34+
## Safe edit boundaries
35+
- For new gameplay/library logic: start in `common/src/main/kotlin/...`, then add loader `actual`/bootstrap only when APIs differ.
36+
- When adding packets/events/config sections, mirror existing object-singleton style (`Archie`, `AEvents`, `ArchieNetworkChannel`) rather than introducing DI/service containers.
37+
- If adding new runtime libraries to shipped jars, use `bundleRuntimeLibrary(...)` / `bundleMod(...)` in loader `build.gradle.kts` files (not plain `implementation` only).

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Archie
2+
3+
Archie is a Kotlin-first Architectury library mod for Minecraft 1.21.1.
4+
It provides shared utilities used by Kernel Panic mods across Fabric and NeoForge.
5+
6+
## Module layout
7+
8+
- `common/`: shared APIs and core implementation (`Archie`, networking, config, GUI, data helpers)
9+
- `fabric/`: Fabric entrypoints, run configs, platform `actual` implementations
10+
- `neoforge/`: NeoForge entrypoints, run configs, platform `actual` implementations
11+
12+
The initialization flow is:
13+
14+
1. Loader entrypoint (`ArchieFabric` or `ArchieNeoForge`)
15+
2. `Archie.init()` in `common`
16+
3. Shared systems register events/network/config and optional datagen/gametest hooks
17+
18+
## Common workflows
19+
20+
```bash
21+
./gradlew build
22+
./gradlew fabric:runClient
23+
./gradlew neoforge:runClient
24+
./gradlew fabric:runDatagen
25+
./gradlew neoforge:runDatagen
26+
./gradlew fabric:runGametest
27+
./gradlew neoforge:runGametest
28+
```
29+
30+
`build` and `assemble` finalize with `fusejars` (merged Fabric+NeoForge artifact).
31+
32+
## Conventions that matter
33+
34+
- Add gameplay/library logic in `common` first, then platform-specific `actual` code only when needed.
35+
- Keep `expect/actual` triplets named `*.common.kt`, `*.fabric.kt`, `*.neoforge.kt`.
36+
- Register packet classes and handlers before calling `register()` on `NetworkChannel`.
37+
- Keep loader manifests tokenized (`${mod_id}`, `${versions.*}`); values come from Gradle properties/version catalog.
38+
- Use `bundleRuntimeLibrary(...)` / `bundleMod(...)` for shipped runtime deps in loader modules.
39+
40+
## Key files
41+
42+
- `common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt`
43+
- `common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt`
44+
- `fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt`
45+
- `neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt`
46+
- `gradle/libs.versions.toml`
47+

common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ object Archie
6262
{
6363
if (Platform.isMinecraftForge())
6464
error("LexForge is not supported. Switch to NeoForge, or don't use my mods.")
65+
// Register mod-scoped hooks first so downstream modules can subscribe during init.
6566
AEvents += MOD
67+
// Register packet handlers before any features try to send packets.
6668
ArchieNetworkChannel.init()
6769
BlockEntityStateManager.init()
6870

@@ -72,6 +74,7 @@ object Archie
7274
Config.init()
7375

7476

77+
// Datagen and GameTest code paths are only activated in dedicated run configs.
7578
if (AGameTestPlatform.isGameTest)
7679
ArchieGameTest.init()
7780
if (ADataGeneratorPlatform.isDataGen)

common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package net.kernelpanicsoft.archie.data
22

3-
3+
/**
4+
* Cross-loader datagen switch.
5+
*
6+
* Loader implementations resolve this from run configuration system properties.
7+
*/
48
expect object ADataGeneratorPlatform
59
{
610
val isDataGen: Boolean

common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ open class AModelBuilder<T : AModelBuilder<T>>(location: ResourceLocation) : AMo
180180
return rootTransforms.apply(block)
181181
}
182182

183-
private fun BlockElement.uvsByFace(face: Direction): FloatArray
183+
private fun BlockElement.computeUvsByFace(face: Direction): FloatArray
184184
{
185185
when (face)
186186
{
@@ -339,7 +339,7 @@ open class AModelBuilder<T : AModelBuilder<T>>(location: ResourceLocation) : AMo
339339

340340
val faceObj = JsonObject()
341341
faceObj.addProperty("texture", serializeLocOrKey(face.texture))
342-
if (!face.uv.uvs.contentEquals(part.uvsByFace(dir)))
342+
if (!face.uv.uvs.contentEquals(part.computeUvsByFace(dir)))
343343
{
344344
faceObj.add("uv", Gson().toJsonTree(face.uv.uvs))
345345
}

common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,18 @@ object TransformationHelper
204204
)
205205

206206
val matrix = Transformation(translation, leftRot, scale, rightRot)
207-
return matrix.applyOrigin(Vector3f(origin))
207+
return matrix.applyOriginLocal(Vector3f(origin))
208208
}
209209

210-
fun Transformation.isIdentity(): Boolean
210+
fun Transformation.isIdentityLocal(): Boolean
211211
{
212-
return this@isIdentity == Transformation.identity()
212+
return this@isIdentityLocal == Transformation.identity()
213213
}
214214

215-
fun Transformation.applyOrigin(origin: Vector3f): Transformation
215+
fun Transformation.applyOriginLocal(origin: Vector3f): Transformation
216216
{
217-
val transform: Transformation = this@applyOrigin
218-
if (transform.isIdentity()) return Transformation.identity()
217+
val transform: Transformation = this@applyOriginLocal
218+
if (transform.isIdentityLocal()) return Transformation.identity()
219219

220220
val ret = transform.matrix
221221
val tmp = Matrix4f().translation(origin.x(), origin.y(), origin.z())

common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@ import dev.architectury.platform.Mod
99

1010
object AEvents
1111
{
12+
/** Fired during datagen runs; handlers should gate by owning [Mod]. */
1213
val GATHER_DATA: Event<GatherDataHandler> = EventFactory.createEventResult()
1314

15+
/** Fired during gametest registration runs; handlers should register test classes per [Mod]. */
1416
val REGISTER_GAME_TEST: Event<RegisterGameTestHandler> = EventFactory.createEventResult()
1517

16-
val MODS: List<Mod> = mutableListOf()
18+
private val mods: MutableList<Mod> = mutableListOf()
1719

18-
fun register(mod: Mod) = (MODS as MutableList<Mod>).add(mod).let { }
20+
/** Mods that opted into Archie event plumbing via `AEvents += MOD`. */
21+
val MODS: List<Mod>
22+
get() = mods
23+
24+
fun register(mod: Mod)
25+
{
26+
mods.add(mod)
27+
}
1928

2029
operator fun plusAssign(mod: Mod) = register(mod)
2130

2231
interface Handler<T>
23-
32+
2433
fun interface HandlerConstructor<T, H : Handler<T>>
2534
{
2635
fun create(mod: Mod, block: T.() -> Unit): H

common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ package net.kernelpanicsoft.archie.gametest
22

33
import dev.architectury.platform.Mod
44

5+
/**
6+
* Cross-loader GameTest integration point.
7+
*
8+
* Implementations detect whether GameTest mode is active and collect test classes per mod.
9+
*/
510
expect object AGameTestPlatform
611
{
712
val isGameTest: Boolean
813

14+
/** Register a test class for [mod] when GameTest bootstrapping occurs. */
915
fun register(clazz: Class<*>, mod: Mod)
1016
}

common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ object BlockEntityStatePacketRegistry {
7676
/**
7777
* Extension function to convert SerializedValue back to its original Kotlin type.
7878
*/
79+
@OptIn(ExperimentalSerializationApi::class)
7980
internal fun BlockEntityStatePacket.SerializedValue.deserialize(serializer: KSerializer<out Any>? = null): Any? = when (this) {
8081
is BlockEntityStatePacket.SerializedValue.IntValue -> this.value
8182
is BlockEntityStatePacket.SerializedValue.StringValue -> this.value

common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,12 @@ open class NetworkChannel(private val id: ResourceLocation) {
146146
fun <T : Any> toServer(vararg packets: T) {
147147
require(packets.isNotEmpty()) { "You need to specify one or more packets to send" }
148148
packets.map {
149-
@Suppress("UNCHECKED_CAST")
150-
val klass = serverClasses.find { x -> x == it::class } as? KClass<T>
151-
?: throw IllegalStateException("Trying to send a packet to server but it hasn't registered the packet and its handler")
152-
val index = serverClasses.indexOf(klass)
153-
val bytes = SerializationManager.cbor.encodeToByteArray(klass.serializer(), it)
154-
155-
Payload(id.withSuffix("_client"), index, bytes)
149+
createPayload(
150+
packet = it,
151+
classes = serverClasses,
152+
payloadId = id.withSuffix("_client"),
153+
missingMessage = "Trying to send a packet to server but it hasn't registered the packet and its handler",
154+
)
156155
}.forEach { NetworkManager.sendToServer(it) }
157156
}
158157

@@ -270,14 +269,45 @@ open class NetworkChannel(private val id: ResourceLocation) {
270269
@Suppress("UNCHECKED_CAST")
271270
private fun <T : Any> createPayloads(packets: Array<out T>): List<Payload> {
272271
return packets.map {
273-
val klass = clientClasses.find { x -> x == it::class } as? KClass<T>
274-
?: throw IllegalStateException("Trying to send a packet to clients but client hasn't registered the packet and its handler")
275-
val index = clientClasses.indexOf(klass)
276-
val bytes = SerializationManager.cbor.encodeToByteArray(klass.serializer(), it)
277-
Payload(id.withSuffix("_server"), index, bytes)
272+
createPayload(
273+
packet = it,
274+
classes = clientClasses,
275+
payloadId = id.withSuffix("_server"),
276+
missingMessage = "Trying to send a packet to clients but client hasn't registered the packet and its handler",
277+
)
278278
}
279279
}
280280

281+
@Suppress("UNCHECKED_CAST")
282+
private fun <T : Any> createPayload(
283+
packet: T,
284+
classes: List<KClass<*>>,
285+
payloadId: ResourceLocation,
286+
missingMessage: String,
287+
): Payload {
288+
val klass = classes.find { it == packet::class } as? KClass<T>
289+
?: throw IllegalStateException(missingMessage)
290+
val index = classes.indexOf(klass)
291+
val bytes = SerializationManager.cbor.encodeToByteArray(klass.serializer(), packet)
292+
return Payload(payloadId, index, bytes)
293+
}
294+
295+
@Suppress("UNCHECKED_CAST")
296+
private fun decodeDispatchData(
297+
payload: Payload,
298+
classes: List<KClass<*>>,
299+
handlers: List<PacketHandler<*>>,
300+
missingClassMessage: String,
301+
missingHandlerMessage: String,
302+
): Pair<Any, PacketHandler<Any>> {
303+
val klass = classes.getOrNull(payload.index)
304+
?: throw NoSuchElementException(missingClassMessage)
305+
val handler = handlers.getOrNull(payload.index) as? PacketHandler<Any>
306+
?: throw NoSuchElementException(missingHandlerMessage)
307+
val msg = SerializationManager.cbor.decodeFromByteArray(klass.serializer(), payload.data)
308+
return msg to handler
309+
}
310+
281311
private fun makeClientboundPacket(vararg payloads: CustomPacketPayload): Packet<*> {
282312
return if (payloads.size == 1) ClientboundCustomPayloadPacket(payloads.first())
283313
else ClientboundBundlePacket(payloads.map { ClientboundCustomPayloadPacket(it) })
@@ -292,23 +322,27 @@ open class NetworkChannel(private val id: ResourceLocation) {
292322
@Suppress("UNCHECKED_CAST")
293323
fun register() {
294324
NetworkManager.registerReceiver(NetworkManager.Side.S2C, serverPacketId, PayloadCodec) { payload, ctx ->
295-
val klass = clientClasses.getOrNull(payload.index)
296-
?: throw NoSuchElementException("No class was found on the clientside. Did you forget to do clientbound?")
297-
val handler = clientboundHandlers.getOrNull(payload.index) as? PacketHandler<Any>
298-
?: throw NoSuchElementException("No handler was found on the clientside. Did you forget to do clientbound?")
299-
val msg = SerializationManager.cbor.decodeFromByteArray(klass.serializer(), payload.data)
325+
val (msg, handler) = decodeDispatchData(
326+
payload = payload,
327+
classes = clientClasses,
328+
handlers = clientboundHandlers,
329+
missingClassMessage = "No class was found on the clientside. Did you forget to do clientbound?",
330+
missingHandlerMessage = "No handler was found on the clientside. Did you forget to do clientbound?",
331+
)
300332
handler(msg, object : IPacketContext {
301333
override val player: Player get() = ctx.player
302334
override val registryAccess: RegistryAccess get() = ctx.registryAccess()
303335
})
304336
}
305337

306338
NetworkManager.registerReceiver(NetworkManager.Side.C2S, clientPacketId, PayloadCodec) { payload, ctx ->
307-
val klass = serverClasses.getOrNull(payload.index)
308-
?: throw NoSuchElementException("No class was found on the serverside. Did you forget to do serverbound?")
309-
val handler = serverboundHandlers.getOrNull(payload.index) as? PacketHandler<Any>
310-
?: throw NoSuchElementException("No handler was found on the serverside. Did you forget to do serverbound?")
311-
val msg = SerializationManager.cbor.decodeFromByteArray(klass.serializer(), payload.data)
339+
val (msg, handler) = decodeDispatchData(
340+
payload = payload,
341+
classes = serverClasses,
342+
handlers = serverboundHandlers,
343+
missingClassMessage = "No class was found on the serverside. Did you forget to do serverbound?",
344+
missingHandlerMessage = "No handler was found on the serverside. Did you forget to do serverbound?",
345+
)
312346
handler(msg, object : IPacketContext {
313347
override val player: Player get() = ctx.player
314348
override val registryAccess: RegistryAccess get() = ctx.registryAccess()

0 commit comments

Comments
 (0)