Skip to content

Commit 95740dc

Browse files
committed
Add ExampleItem and LoadService implementation, refactor ProjectProps, and enhance build configuration
Took 3 hours 47 minutes skip-checks: true
1 parent f8e609b commit 95740dc

16 files changed

Lines changed: 447 additions & 150 deletions

File tree

.idea/scopes/Main_Code.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@ dependencies {
5353
compileOnly("io.github.llamalad7:mixinextras-common:0.3.5")
5454
compileOnly("net.minecraft:client:$minecraftVersion")
5555
annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")
56+
compileOnly(kotlin("reflect"))
57+
compileOnly("org.ow2.asm:asm:9.6")
5658
}

common/src/main/java/com/example/mixin/ExampleMixin.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.example.mixin;
22

3-
import com.example.Properties;
3+
import com.example.ProjectProps;
44
import org.spongepowered.asm.mixin.Mixin;
55
import org.spongepowered.asm.mixin.injection.At;
66
import org.spongepowered.asm.mixin.injection.Inject;
@@ -13,6 +13,6 @@ public class ExampleMixin {
1313
private void init(CallbackInfo info) {
1414
// This code is injected into the start of MinecraftServer.loadLevel()V
1515
System.out.println("Hello from ExampleMixin! The server is starting up.");
16-
System.out.println("ModID: " + Properties.getSafe("modId") + ", ModName: " + Properties.getSafe("modName"));
16+
System.out.println("ModID: " + ProjectProps.getSafe("modId") + ", ModName: " + ProjectProps.getSafe("modName"));
1717
}
1818
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example
2+
3+
import com.example.utils.LoadUtil
4+
import net.minecraft.world.item.Item
5+
import net.minecraft.world.level.block.Block
6+
import org.slf4j.Logger
7+
import org.slf4j.LoggerFactory
8+
9+
object Globals {
10+
val logger: Logger = LoggerFactory.getLogger(ProjectProps["modName"])
11+
12+
val items: MutableMap<String, (props: Item.Properties) -> Item> = mutableMapOf()
13+
val blocks: MutableMap<String, () -> Block> = mutableMapOf()
14+
15+
fun loadAssets() {
16+
LoadUtil.loadAll("com.example.items")
17+
LoadUtil.loadAll("com.example.blocks")
18+
info("Finished loading all items and blocks from com.example")
19+
}
20+
}
21+
22+
fun info(message: String, vararg args: Any?) {
23+
Globals.logger.info(message, *args)
24+
}

common/src/main/kotlin/com/example/Properties.kt renamed to common/src/main/kotlin/com/example/ProjectProps.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.example
22

33
import java.util.Properties as JavaProperties
44

5-
object Properties {
5+
object ProjectProps {
66
private val props: Map<String, String>
77
// This must be available at compile time because it is used in annotations,
88
// which is why it can only be checked and not automatic.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.items
2+
3+
import com.example.Globals
4+
import com.example.utils.LoadKt
5+
import net.minecraft.world.InteractionResult
6+
import net.minecraft.world.item.Item
7+
import net.minecraft.world.item.context.UseOnContext
8+
9+
@LoadKt
10+
class ExampleItem(props: Properties) : Item(
11+
props
12+
) {
13+
override fun useOn(ctx: UseOnContext): InteractionResult {
14+
println("ExampleItem used at position: ${ctx.clickedPos}")
15+
return InteractionResult.PASS
16+
}
17+
18+
companion object {
19+
fun load() {
20+
println("Loading ExampleItem...")
21+
Globals.items["example_item"] = { props: Properties -> ExampleItem(props) }
22+
}
23+
}
24+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.example.utils
2+
3+
import org.objectweb.asm.*
4+
import java.lang.reflect.Field
5+
import java.lang.reflect.Method
6+
import java.lang.reflect.Modifier
7+
import java.util.*
8+
9+
@Retention(AnnotationRetention.RUNTIME)
10+
@Target(AnnotationTarget.CLASS)
11+
annotation class LoadKt
12+
13+
@Retention(AnnotationRetention.RUNTIME)
14+
@Target(AnnotationTarget.CLASS)
15+
annotation class LoadJ
16+
17+
interface LoadService {
18+
fun loadAll(packageName: String)
19+
}
20+
21+
object LoadUtil {
22+
fun loadAll(packageName: String) {
23+
val loader: ServiceLoader<LoadService> = ServiceLoader.load(LoadService::class.java)
24+
for (service in loader) {
25+
service.loadAll(packageName)
26+
}
27+
}
28+
29+
private val loadedClasses = mutableSetOf<String>()
30+
fun processClass(className: String, classLoader: ClassLoader = Thread.currentThread().contextClassLoader) {
31+
if (loadedClasses.contains(className)) {
32+
return // Class already processed, this is probably a companion object
33+
}
34+
loadedClasses.add(className)
35+
if (hasAnnotation(className, "Lcom/example/utils/LoadKt;")) {
36+
val clazz = classLoader.loadClass(className)
37+
if (clazz.isAnnotationPresent(LoadJ::class.java)) {
38+
throw Exception("Class $clazz cannot be annotated with both LoadKt and LoadJ")
39+
}
40+
41+
if (clazz.kotlin.isCompanion) {
42+
throw Exception(
43+
"Class $clazz cannot be a companion object. " +
44+
"Please keep the companion object and move the annotation to the class."
45+
)
46+
}
47+
48+
val instance: Field? = try {
49+
clazz.getDeclaredField("INSTANCE")
50+
} catch (_: NoSuchFieldException) {
51+
try {
52+
clazz.getDeclaredField("Companion")
53+
} catch (_: NoSuchFieldException) {
54+
null
55+
}
56+
}
57+
58+
if (instance == null) {
59+
throw Exception("Class $clazz annotated with LoadKt must have a field named INSTANCE or Companion")
60+
}
61+
62+
if (!Modifier.isStatic(instance.modifiers)) {
63+
throw Exception("Field INSTANCE or Companion in class $clazz annotated with LoadKt must be static")
64+
}
65+
66+
val loadMethod: Method? = try {
67+
instance.type.getDeclaredMethod("load")
68+
} catch (_: NoSuchMethodException) {
69+
null
70+
}
71+
72+
checkMethod(loadMethod)
73+
74+
loadMethod!!.invoke(instance.get(null))
75+
} else if (hasAnnotation(className, "Lcom/example/utils/LoadJ;")) {
76+
val clazz = classLoader.loadClass(className)
77+
val loadMethod: Method = try {
78+
clazz.getDeclaredMethod("load")
79+
} catch (_: NoSuchMethodException) {
80+
throw Exception("Class $clazz annotated with LoadJ must have a method named load")
81+
}
82+
83+
checkMethod(loadMethod)
84+
85+
if (!Modifier.isStatic(loadMethod.modifiers)) {
86+
throw Exception("Method ${loadMethod.name} in class ${loadMethod.declaringClass} must be static")
87+
}
88+
89+
loadMethod.invoke(null)
90+
}
91+
}
92+
93+
private fun checkMethod(method: Method?) {
94+
if (method == null) {
95+
throw Exception("Method must not be null")
96+
}
97+
98+
if (method.parameterCount != 0) {
99+
throw Exception("Method ${method.name} in class ${method.declaringClass} must have no parameters")
100+
}
101+
102+
if (method.returnType != Void.TYPE) {
103+
throw Exception("Method ${method.name} in class ${method.declaringClass} must return Void")
104+
}
105+
}
106+
107+
@Throws(java.lang.Exception::class)
108+
fun hasAnnotation(className: String, annotationDescriptor: String): Boolean {
109+
val found = booleanArrayOf(false)
110+
val stream = this::class.java.classLoader.getResourceAsStream(className.replace('.', '/') + ".class")
111+
val reader = try { ClassReader(stream) } catch (_: Exception) {
112+
throw Exception("Class $className not found or cannot be read")
113+
}
114+
reader.accept(object : ClassVisitor(Opcodes.ASM9) {
115+
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? {
116+
if (desc == annotationDescriptor) {
117+
found[0] = true
118+
}
119+
return super.visitAnnotation(desc, visible)
120+
}
121+
}, 0)
122+
return found[0]
123+
}
124+
}
125+

fabric/src/main/kotlin/com/example/ExampleMod.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ object ExampleMod : ModInitializer {
1111
// However, some things (like resources) may still be uninitialized.
1212
// Proceed with mild caution.
1313
logger.info("Hello Fabric world!")
14+
Globals.loadAssets()
1415
}
1516
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.example.utils
2+
3+
import com.example.utils.LoadUtil.processClass
4+
import java.io.File
5+
import java.net.URI
6+
import java.net.URL
7+
import java.util.*
8+
import java.util.jar.JarFile
9+
10+
class LoadServiceImpl : LoadService {
11+
override fun loadAll(packageName: String) {
12+
val classLoader = Thread.currentThread().contextClassLoader
13+
val path = packageName.replace('.', '/')
14+
15+
val resources: Enumeration<URL> = classLoader.getResources(path)
16+
while (resources.hasMoreElements()) {
17+
val resource = resources.nextElement()
18+
val urlStr = resource.toExternalForm()
19+
20+
if (urlStr.startsWith("file:")) {
21+
// Handle directory based class loading
22+
val directory = File(URI(urlStr))
23+
scanDirectory(directory, packageName, path) { className ->
24+
processClass(className)
25+
}
26+
} else if (urlStr.startsWith("jar:")) {
27+
// Handle JAR based class loading
28+
val jarPath = urlStr.substring(4, urlStr.indexOf("!")).replace("file:", "")
29+
JarFile(jarPath).use { jarFile ->
30+
val entries = jarFile.entries()
31+
while (entries.hasMoreElements()) {
32+
val entry = entries.nextElement()
33+
if (entry.name.startsWith(path) && entry.name.endsWith(".class")) {
34+
val className = entry.name
35+
.replace("/", ".")
36+
.substring(0, entry.name.length - 6) // remove .class
37+
processClass(className)
38+
}
39+
}
40+
}
41+
} else {
42+
throw Exception("Unsupported resource type: $urlStr. Only file and jar resources are supported.")
43+
}
44+
}
45+
}
46+
47+
private fun scanDirectory(dir: File, packageName: String, packagePath: String, processClass: (String) -> Unit) {
48+
if (!dir.exists()) return
49+
50+
dir.listFiles()?.forEach { file ->
51+
if (file.isDirectory) {
52+
scanDirectory(file, "$packageName.${file.name}", "$packagePath/${file.name}", processClass)
53+
} else if (file.name.endsWith(".class")) {
54+
val className = packageName + "." + file.name.substring(0, file.name.length - 6)
55+
processClass(className)
56+
}
57+
}
58+
}
59+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.example.utils.LoadServiceImpl

0 commit comments

Comments
 (0)