Skip to content

Commit 2f7c2a6

Browse files
committed
Fix particle-detection logic
1 parent 2e97c43 commit 2f7c2a6

4 files changed

Lines changed: 48 additions & 130 deletions

File tree

src/main/java/jp/s12kuma01/celeritasextra/client/gui/CeleritasExtraGameOptionPages.java

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

33
import com.google.common.collect.ImmutableList;
44
import jp.s12kuma01.celeritasextra.client.particle.ParticleClassRegistry;
5+
import net.minecraft.client.Minecraft;
56
import net.minecraft.client.resources.I18n;
67
import org.embeddedt.embeddium.impl.gui.framework.TextComponent;
78
import org.taumc.celeritas.api.options.control.ControlValueFormatter;
@@ -138,8 +139,8 @@ public static OptionPage particle() {
138139
opts -> opts.particleSettings.blockBreaking))
139140
.build());
140141

141-
// Scan mod jars via ASM to discover all Particle subclasses
142-
ParticleClassRegistry.getInstance().scanModJars();
142+
// Scan registered particle factories to discover particle classes
143+
ParticleClassRegistry.getInstance().scanFactories(Minecraft.getMinecraft().effectRenderer);
143144

144145
// Dynamic particle class controls - grouped by mod name
145146
var discovered = ParticleClassRegistry.getInstance().getDiscoveredClasses();

src/main/java/jp/s12kuma01/celeritasextra/client/particle/ParticleClassRegistry.java

Lines changed: 29 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
11
package jp.s12kuma01.celeritasextra.client.particle;
22

3+
import jp.s12kuma01.celeritasextra.mixin.particle.IMixinParticleManager;
4+
import net.minecraft.client.particle.IParticleFactory;
35
import net.minecraft.client.particle.Particle;
4-
import net.minecraftforge.fml.common.Loader;
5-
import net.minecraftforge.fml.common.ModContainer;
6-
import org.objectweb.asm.ClassReader;
7-
8-
import java.io.File;
9-
import java.io.IOException;
10-
import java.io.InputStream;
11-
import java.nio.file.FileVisitResult;
12-
import java.nio.file.Files;
13-
import java.nio.file.Path;
14-
import java.nio.file.SimpleFileVisitor;
15-
import java.nio.file.attribute.BasicFileAttributes;
6+
import net.minecraft.client.particle.ParticleManager;
7+
8+
import java.lang.reflect.Method;
169
import java.util.*;
1710
import java.util.concurrent.ConcurrentHashMap;
18-
import java.util.jar.JarEntry;
19-
import java.util.jar.JarFile;
2011

2112
/**
2213
* Runtime registry for discovered particle classes.
@@ -122,126 +113,36 @@ public String[] getDiscoveredClassesArray() {
122113
}
123114

124115
/**
125-
* Scan all mod jar files using ASM to discover Particle subclasses
126-
* without loading or instantiating them. Uses bytecode superclass analysis
127-
* to find classes that extend Particle or any known Particle subclass.
128-
* Also records which mod each class belongs to.
116+
* Scan ParticleManager's registered factories to discover particle classes.
117+
* Uses two strategies:
118+
* 1. Enclosing class - for inner class factories (e.g. ParticleFlame.Factory)
119+
* 2. Covariant return type - for factories that declare a specific Particle subclass return type
129120
*/
130-
public void scanModJars() {
131-
// Build initial set of known particle class names (internal form: / separator)
132-
Set<String> knownParticleNames = new HashSet<>();
133-
knownParticleNames.add(Particle.class.getName().replace('.', '/'));
134-
for (String className : discoveredClasses.keySet()) {
135-
knownParticleNames.add(className.replace('.', '/'));
136-
}
137-
138-
// Collect superclass map and class-to-mod mapping from mod sources using ASM
139-
Map<String, String> superMap = new HashMap<>();
140-
Map<String, String> classToModName = new HashMap<>();
141-
Set<File> scannedSources = new HashSet<>();
142-
143-
for (ModContainer mod : Loader.instance().getModList()) {
144-
File source = mod.getSource();
145-
if (source == null || !scannedSources.add(source)) continue;
146-
147-
String modName = mod.getModId();
148-
149-
if (source.isFile() && source.getName().endsWith(".jar")) {
150-
scanJarForSuperclasses(source, superMap, classToModName, modName);
151-
} else if (source.isDirectory()) {
152-
scanDirectoryForSuperclasses(source, superMap, classToModName, modName);
121+
public void scanFactories(ParticleManager particleManager) {
122+
Map<Integer, IParticleFactory> factories = ((IMixinParticleManager) particleManager).getParticleTypes();
123+
124+
for (var entry : factories.entrySet()) {
125+
IParticleFactory factory = entry.getValue();
126+
Class<?> factoryClass = factory.getClass();
127+
128+
// Strategy 1: Enclosing class (inner class factories like ParticleFlame.Factory)
129+
Class<?> enclosingClass = factoryClass.getEnclosingClass();
130+
if (enclosingClass != null && Particle.class.isAssignableFrom(enclosingClass)) {
131+
recordClass(enclosingClass.getName(), enclosingClass.getSimpleName());
132+
continue;
153133
}
154-
}
155134

156-
// Fallback: scan the jar/directory containing Particle.class itself.
157-
// MinecraftDummyContainer.getSource() returns a non-existent "minecraft.jar",
158-
// so vanilla Particle subclasses are missed by the mod list scan above.
159-
try {
160-
var codeSource = Particle.class.getProtectionDomain().getCodeSource();
161-
if (codeSource != null) {
162-
File particleSource = new File(codeSource.getLocation().toURI());
163-
if (scannedSources.add(particleSource)) {
164-
if (particleSource.isFile() && particleSource.getName().endsWith(".jar")) {
165-
scanJarForSuperclasses(particleSource, superMap, classToModName, "minecraft");
166-
} else if (particleSource.isDirectory()) {
167-
scanDirectoryForSuperclasses(particleSource, superMap, classToModName, "minecraft");
135+
// Strategy 2: Covariant return type analysis
136+
try {
137+
for (Method method : factoryClass.getDeclaredMethods()) {
138+
Class<?> returnType = method.getReturnType();
139+
if (returnType != Particle.class && Particle.class.isAssignableFrom(returnType)) {
140+
recordClass(returnType.getName(), returnType.getSimpleName());
141+
break;
168142
}
169143
}
144+
} catch (Throwable _) {
170145
}
171-
} catch (Exception ignored) {
172-
}
173-
174-
// Assign mod names to already-discovered classes (from config or runtime recording)
175-
for (String fullName : discoveredClasses.keySet()) {
176-
String internalName = fullName.replace('.', '/');
177-
String modName = classToModName.get(internalName);
178-
if (modName != null) {
179-
recordModName(fullName, modName);
180-
}
181-
}
182-
183-
// Iteratively resolve: find classes whose superclass is a known particle class
184-
boolean changed = true;
185-
while (changed) {
186-
changed = false;
187-
for (var entry : superMap.entrySet()) {
188-
String className = entry.getKey();
189-
String superName = entry.getValue();
190-
if (knownParticleNames.contains(className)) continue;
191-
if (superName != null && knownParticleNames.contains(superName)) {
192-
String fullName = className.replace('/', '.');
193-
String simpleName = toSimpleName(fullName);
194-
recordClass(fullName, simpleName);
195-
String modName = classToModName.get(className);
196-
if (modName != null) {
197-
recordModName(fullName, modName);
198-
}
199-
knownParticleNames.add(className);
200-
changed = true;
201-
}
202-
}
203-
}
204-
}
205-
206-
private void scanJarForSuperclasses(File jarFile, Map<String, String> superMap,
207-
Map<String, String> classToModName, String modName) {
208-
try (JarFile jar = new JarFile(jarFile)) {
209-
Enumeration<JarEntry> entries = jar.entries();
210-
while (entries.hasMoreElements()) {
211-
JarEntry entry = entries.nextElement();
212-
if (!entry.getName().endsWith(".class")) continue;
213-
try (InputStream is = jar.getInputStream(entry)) {
214-
ClassReader cr = new ClassReader(is);
215-
String className = cr.getClassName();
216-
superMap.put(className, cr.getSuperName());
217-
classToModName.put(className, modName);
218-
} catch (Throwable _) {
219-
}
220-
}
221-
} catch (Throwable _) {
222-
}
223-
}
224-
225-
private void scanDirectoryForSuperclasses(File dir, Map<String, String> superMap,
226-
Map<String, String> classToModName, String modName) {
227-
Path root = dir.toPath();
228-
try {
229-
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
230-
@Override
231-
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
232-
if (file.toString().endsWith(".class")) {
233-
try (InputStream is = Files.newInputStream(file)) {
234-
ClassReader cr = new ClassReader(is);
235-
String className = cr.getClassName();
236-
superMap.put(className, cr.getSuperName());
237-
classToModName.put(className, modName);
238-
} catch (Throwable _) {
239-
}
240-
}
241-
return FileVisitResult.CONTINUE;
242-
}
243-
});
244-
} catch (IOException ignored) {
245146
}
246147
}
247148

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package jp.s12kuma01.celeritasextra.mixin.particle;
2+
3+
import net.minecraft.client.particle.IParticleFactory;
4+
import net.minecraft.client.particle.ParticleManager;
5+
import org.spongepowered.asm.mixin.Mixin;
6+
import org.spongepowered.asm.mixin.gen.Accessor;
7+
8+
import java.util.Map;
9+
10+
@Mixin(ParticleManager.class)
11+
public interface IMixinParticleManager {
12+
13+
@Accessor("particleTypes")
14+
Map<Integer, IParticleFactory> getParticleTypes();
15+
}

src/main/resources/celeritasextra.default.mixin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"client": [
1212
"animation.MixinTextureMap",
1313
"light_updates.MixinWorld",
14+
"particle.IMixinParticleManager",
1415
"particle.MixinParticleManager",
1516
"particle.MixinParticleFirework",
1617
"particle.MixinRenderGlobal",

0 commit comments

Comments
 (0)