Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a detailed Mixin tutorial in advanced area. #10

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.vs/Documentation/FileContentIndex
/.vs/Documentation/v17
/.vs
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the first two necessary if you have the third?

148 changes: 148 additions & 0 deletions docs/advanced/mixin/1.preparation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
Preparation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs an introduction as to what mixins are. Why would I do this if I don't even know what the purpose of mixins are for?

===========

Edit Your ```build.gradle```
----------------------

Apply the mixin plugin.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should explain that the plugin is available on whatever maven is mirroring it. I imagine NeoForged unless they've added it to maven central recently.

```groovy
plugins {
id 'org.spongepowered.mixin' version '0.7.+'
}
```

Add run arguments to gradle tasks.
```groovy
minecraft {
runs {
client {
property 'forge.enabledGameTestNamespaces', mod_id
args "-mixin.config=${mod_id}.mixins.json" // add this line
}
server {
property 'forge.enabledGameTestNamespaces', mod_id
args '--nogui'
args "-mixin.config=${mod_id}.mixins.json" // add this line
}
data {
workingDirectory project.file('run-data')
args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
args "-mixin.config=${mod_id}.mixins.json" // add this line
}
}
}
Comment on lines +16 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of these arguments are unnecessary. Additionally, what does it mean to specify the mixin config name? Where does the relative path point? Can a subfolder be used?

```

Add the mixin config block before ```repositories``` block
```groovy
mixin {
add sourceSets.main, "${mod_id}.refmap.json"
}
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What purpose does this serve?

```

Add the mixin annotation processors in the ```dependencies``` block
```groovy
dependencies {
minecraft "net.neoforged:forge:${minecraft_version}-${neo_version}"
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' // add this line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why?

}
```

Add the mixin config file in the manifest
```groovy
'Specification-Title' : mod_id,
'Specification-Vendor' : mod_authors,
'Specification-Version' : '1', // We are version 1 of ourselves
'Implementation-Title' : project.name,
'Implementation-Version' : project.jar.archiveVersion,
'Implementation-Vendor' : mod_authors,
'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
Comment on lines +53 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary and doesn't show where it's supposed to be.

'MixinConfigs': "${mod_id}.mixins.json" // add this line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary here?

```
Save the file and sync the gradle project.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Save the file and sync the gradle project.
Once the configurations are specified, refresh the gradle project and regenerate the runs.


Create Mixin Config Class
---------------------------
All classes related to mixin must be in a separate package, which is called "mixin package". So you can create a package called ```mixin```.

Then create a class called ```MixinPlugin``` in the ```mixin``` package and implement the interface ```IMixinConfigPlugin```. The IDE will instruct you to implement all methods. This will be a mixin config class.

```java
public class MixinPlugin implements IMixinConfigPlugin {
private boolean isModInstalled;
@Override
public void onLoad(String mixinPackage) {
try{
Class.forName("com.example.modid.ExampleMod"); // Your main class
isModInstalled = true;
} catch (ClassNotFoundException e) {
isModInstalled = false;
}
}

@Override
public String getRefMapperConfig() {
return null;
}

@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return isModInstalled;
}

@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}

@Override
public List<String> getMixins() {
return null;
}

@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}

@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}
```
Comment on lines +64 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire section is completely unnecessary for a basic mixin project and doesn't explain anything on why you would use this. Why would I need to check if my mod is installed when my mod is the one that is shipping the mixin class?


Create Mixin Config File
-------------------
The config file will be like this, which can be named "mod_id.mixins.json" and should be in your ```resources``` folder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like what? This is a hardcoded sentence which conflict the above properties which were not explained.

```json
{
"required": true,
"package": "com.example.modid.mixin",
"compatibilityLevel": "JAVA_17",
"minVersion": "0.8",
"refmap": "modid.refmap.json",
"plugin": "com.example.modid.mixin.MixinPlugin",
"mixins": [

],
"client": [

],
"injectors": {
"defaultRequire": 1
}
}
```
You should mainly care about 4 of those properties:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are nine properties which you use. So, you need to care about all nine properties at a minimum. All need to be explained.


```package``` - The path of your mixin package.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more explanation. The package identifier will point to a package as a relative path so that the mixins and client section do not need to specify the canonical path.


```plugin``` - The path of the mixin config class you created above.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, unnecessary by default. Additionally, there should probably be a warning that this needs to be the canonical path as it does not inherit a relative location from the package.


```mixin``` - The array contains your mixin classes that will be applied to both sides.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixins first. Second, needs to mention relative to package.


```client``` - The array contains your mixin classes that will only be applied to the client side.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should say physical client and relative to package.


Here, you have accomplished all the preparations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary line.


:::caution
Please be careful about file references (i.e. package and class names) in those config files. Any mistakes may take the Mixin system down.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be more descriptive.

:::
36 changes: 36 additions & 0 deletions docs/advanced/mixin/2.mixinclass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Mixin Class
===========

A mixin class is a special class that contains mixin codes that be applied to its target class. A mixin class can only have one target class, which is designated by the class annotation ```@Mixin```.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a mixin code? Also, this sentence is false: a mixin can have an array of target classes. You can look a Mixin#value to confirm: https://github.com/SpongePowered/Mixin/blob/155314e6e91465dad727e621a569906a410cd6f4/src/main/java/org/spongepowered/asm/mixin/Mixin.java#L59


None of the mixin classes will be instantiated, as they will only be compiled to bytecodes and then applied in target classes. Therefore, the mixin classes can be abstract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Here is an example of a mixin class.

```java
@Mixin(TargetClass.class)
public abstract class TargetClassMixin {

}
```
Comment on lines +8 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

All mixin classes must be specified in the mixin config file like this, either for the client side only or both sides. Otherwise, they will not take any effects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not completely true either because of the config class above. This should be reworded probably with a 'by default' qualifier and then mention this as one of the ways.


```json
{
"required": true,
"package": "com.example.modid.mixin",
"compatibilityLevel": "JAVA_17",
"minVersion": "0.8",
"refmap": "modid.refmap.json",
"plugin": "com.example.modid.mixin.MixinPlugin",
"mixins": [
"TargetClassMixin"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should reiterate relativization.

],
"client": [

],
"injectors": {
"defaultRequire": 1
}
}
```
133 changes: 133 additions & 0 deletions docs/advanced/mixin/3.inject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
@Inject
======
Injection is the most used technology which can "insert" your codes to a certain place of a certain vanilla method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not completely true either. Injection can happen in a number of places, not only for methods.


Injection is similar to the [Events][events] system, where every event can be considered as an injection point and you can inject your codes through listener methods. But injection is more flexible because:

- The Event system may not cover any situations but mixin injection can cover all vanilla methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixin cannot cover all implementations, there are always edge cases.

- The Event system reuses codes for different objects but mixin injection can be specific to each class.
Comment on lines +5 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be pros and cons here. This glosses over potential compatibilities issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence does not make any sense. You can make events specific for a given class with conditionals.


Basic
-----

### Injection Method

Injection methods are some special methods in Mixin that will not be invoked directly. Their codes will be injected into their target methods. Injection methods are annotated with ```@Inject```. Such an annotation is used to specify the target method, injection position, and other configurations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is not true. Merging the bytecode into the class at runtime will invoke the method created in the mixin directly. There may be a delegate for the callback information, but it is still a direct invocation from what I understand.

Additionally, injection is being used too loosely here since the word can apply for overwrites and redirects here.


Two parameters are mandatory in ```@Inject```:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to be specific, only @At is required by default. This is because of how methods can be added to the injection location through varying means.


- ```method``` - The name of the target method, which should be in the target class. If the method is overloaded, it should be specified by the descriptors, which will be discussed later.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be multiple methods.

- ```at``` - The injection position in the target method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be multiple injection points.


The most common positions for ```at``` are:

- ```HEAD``` - At the start of the method
- ```TAIL``` - At the end of the method
- ```RETURN``` - Before every return statement of the method
Comment on lines +22 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are going to mention an argument, you should mention all possible arguments along with examples.


The name of the injection method can be different from the target method while the types and sequences of the parameters should not be changed. Additionally, there is an extra parameter that should be appended to the parameter list of the injection method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also half-true, the parameters can be different depending on the configurations within the annotation. Additionally, the callback parameter must be the last parameter: https://github.com/SpongePowered/Mixin/wiki/Advanced-Mixin-Usage---Callback-Injectors#2-building-handler-methods


- ```CallbackInfo``` - If the target method is void.
- ```CallbackInfoReturnable<ReturnType>``` - If the target method has a return value.


Here is an example of injection method

```java
@Inject(method = {"methodName"}, at = {@At("HEAD")})
public void onMethodName(Type1 param1, Type2 param2, CallbackInfoReturnable<Boolean> ci){
...
}
```

whose target is
```java
public boolean methodName(Type1 param1, Type2 param2){
...
}
```
Comment on lines +43 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is it being injected into here?


Constructors are also treated as methods with name ```<init>``` but not ```init```. Here are some different situations when injecting into the constructors.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why. Why is <init> the name of the constructor method?


- To inject into non-parameterized constructors, or there is only one constructor no matter whether parameterized or non-parameterized, the method should be ```method = {"<init>"}```.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also be method = "<init>"


- To inject into parameterized and overloaded constructors, the method should be specified by the descriptors, which will be discussed later.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss this first. Explanations should not be pushed off later if its needed to understand how to use it.


- To inject into all constructors, the method should be ```method = {"<init>*"}```.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be a warning here for trying to consume more than one method.


- The injection position of constructors can only be ```TAIL``` or ```RETURN```.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, explain for all cases and not just constructors.


- To inject into static blocks, the method should be ```method = {"<clinit>"}```
Comment on lines +50 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be applied to any method, not just constructors.




### Injection Codes
Injection codes are the codes in the injection methods that will be injected into the target methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this sentence means.


Most time you can complete the injection codes as if they were in the corresponding places of target methods. But to access fields or invoke methods, you should use [Shadow][shadow].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When should I access fields or invoke methods?


Additionally, ```this``` cannot be directly used to refer to the target instance as the codes are in the mixin class. You should cast ```this``` to ```Object``` and then to the target class. It may seem strange elsewhere, but it is normal in Mixin.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why! It's not strange elsewhere, it's bad practice almost anywhere. It's done for a very specific reason.


```java
TargetClass self = (TargetClass) (Object) this;
```

If you want to end the methods in advance, or "return" the target method in your injection codes, you should add an optional parameter ```cancellable``` to the annotation.

```java
@Inject(method = {""}, at = {@At("")}, cancellable = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give a more complete example.

```

Then use ```CallbackInfo#cancel()``` to end the target method if it is void
or use ```CallbackInfoReturnable#setReturnValue(T)``` to set the return value and end the target method.

Advanced
--------

### Method Overloading

Methods with different parameter types can have the same name. This is called method overloading, such as ```methodName(int)``` and ```methodName(int,int)```

To inject into overloaded methods, you should explicitly designate the method name as well as their descriptors in the ```()```. The following [Oracle's document][doc] can help you to finish this. Accordingly, the parameters of their injection methods are also different.

| FieldType term | Type | Interpretation |
|----------------|-----------|-----------------------------------------------------------------------------------|
| B | byte | signed byte |
| C | char | Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16 |
| D | double | double-precision floating-point value |
| F | float | single-precision floating-point value |
| I | int | int integer |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table formatting.

| J | long | long integer |
| L ClassName ; | reference | an instance of class ClassName |
| S | short | signed short |
| Z | boolean | true or false |
| [ | reference | one array dimension |
Comment on lines +87 to +104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already in the AT docs correct? This should probably be moved into its own section then.


Therefore, the injection methods should be:

```java
@Inject(method = {"methodName(I)V"}, at = {@At("HEAD")})
public void onMethodName(int param1, CallbackInfo ci){
...
}
```
and

```java
@Inject(method = {"messageName(II)V"}, at = {@At("HEAD")})
public void onMethodName(int param1, int param2, CallbackInfo ci){
...
}
```
Comment on lines +108 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show target example.


:::tip
For overloaded constructors, the name is always ```method = {"<init>(...)V"}```. What you need to edit is the parameter type in ().
:::
Comment on lines +123 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example!






Comment on lines +126 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty space

[events]: ../../concepts/events.md
[shadow]: 4.shadow.md
[doc]: https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-4.html#jvms-4.3.2
46 changes: 46 additions & 0 deletions docs/advanced/mixin/4.shadow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@Shadow
======

Shadow is a technology that redirects the access of the fields and methods from the mixin classes to the vanilla classes. It allows you to use those elements like normal ones so that your method code can be simplified. It can also help you to access the private fields or methods in the target class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a reword. Shadow acts like a placeholder for the actual field or method call. I wouldn't call it necessarily a redirect because of the bytecode merging replaces the calls.


Fields
------
Shadowed fields are annotated with ```@Shadow``` and have the same access modifiers, names, and types as the vanilla fields in the target class.

```java
@Shadow
<access modifier> <static or not> <type name> <field name>;
```
Comment on lines +10 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example is better here since everyone should know how to create a field in a class.


The Mixin system will automatically redirect your access to the corresponding fields in the target class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really true since it depends on other compatibilities. Edge cases should be documented.



Methods
-------
Shadowed methods are annotated with ```@Shadow``` and have the same access modifiers, names, parameter types, and return types as the vanilla methods in the target class.

If the vanilla method is not static, the shadowed method should be abstract.


```java
@Shadow
<access modifier> abstract <type name> <method name>(parameter list...);
```
Comment on lines +25 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example than abstractness here.


If the vanilla method is static, the shadowed method should be static and have a method body. The return value does not matter because this method is just a placeholder.
```java
@Shadow
<access modifier> static <type name> <method name>(parameter list...){
return null; // for reference types
// return 0; // for primitives
// return false;
}
```
Comment on lines +30 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most people throw an exception.


The Mixin system will automatically redirect your access to the corresponding methods in the target class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really true since it depends on other compatibilities. Edge cases should be documented.


:::tip
Do not change field names, method names, parameter types, and sequences, or the shadow will not work.

You can change parameter names.
:::
Comment on lines +42 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading