mirror of
https://github.com/GeyserExtensionists/GeyserModelEnginePackGenerator.git
synced 2025-12-19 15:09:18 +00:00
Initial commit
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
BIN
libs/geyserutils-geyser-1.0-SNAPSHOT.jar
Normal file
BIN
libs/geyserutils-geyser-1.0-SNAPSHOT.jar
Normal file
Binary file not shown.
66
pom.xml
Normal file
66
pom.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>re.imc</groupId>
|
||||
<artifactId>GeyserModelEnginePackGenerator</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>opencollab-release-repo</id>
|
||||
<url>https://repo.opencollab.dev/maven-releases/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>opencollab-snapshot-repo</id>
|
||||
<url>https://repo.opencollab.dev/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.28</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.geysermc.geyser</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>2.2.3-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.zimzaza4</groupId>
|
||||
<artifactId>geyserutils-geyser</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/libs/geyserutils-geyser-1.0-SNAPSHOT.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,52 @@
|
||||
package re.imc.geysermodelenginepackgenerator;
|
||||
|
||||
import me.zimzaza4.geyserutils.geyser.GeyserUtils;
|
||||
import org.geysermc.event.subscribe.Subscribe;
|
||||
import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent;
|
||||
import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent;
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import re.imc.geysermodelenginepackgenerator.util.ZipUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class ExtensionMain implements Extension {
|
||||
|
||||
private File source;
|
||||
|
||||
@Subscribe
|
||||
public void onLoad(GeyserPreInitializeEvent event) {
|
||||
source = dataFolder().resolve("input").toFile();
|
||||
source.mkdirs();
|
||||
|
||||
File[] files = source.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
String id = "modelengine:" + file.getName();
|
||||
GeyserUtils.addCustomEntity(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Subscribe
|
||||
public void onPackLoad(GeyserLoadResourcePacksEvent event) {
|
||||
|
||||
File generatedPack = dataFolder().resolve("generated_pack").toFile();
|
||||
|
||||
GeneratorMain.startGenerate(source, generatedPack);
|
||||
|
||||
|
||||
Path generatedPackZip = dataFolder().resolve("generated_pack.zip");
|
||||
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(generatedPackZip))) {
|
||||
// 压缩文件夹
|
||||
ZipUtil.compressFolder(generatedPack, generatedPack.getName(), zipOutputStream);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
event.resourcePacks().add(generatedPackZip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package re.imc.geysermodelenginepackgenerator;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParser;
|
||||
import re.imc.geysermodelenginepackgenerator.generator.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class GeneratorMain {
|
||||
public static final Map<String, Entity> entityMap = new HashMap<>();
|
||||
public static final Map<String, Animation> animationMap = new HashMap<>();
|
||||
public static final Map<String, Geometry> geometryMap = new HashMap<>();
|
||||
public static final Map<String, Texture> textureMap = new HashMap<>();
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
File source = new File(args.length > 0 ? args[0] : "input");
|
||||
|
||||
File output = new File("output");
|
||||
|
||||
startGenerate(source, output);
|
||||
}
|
||||
|
||||
public static void startGenerate(File source, File output) {
|
||||
|
||||
|
||||
for (File file1 : source.listFiles()) {
|
||||
if (file1.isDirectory()) {
|
||||
if (file1.listFiles() == null) {
|
||||
continue;
|
||||
}
|
||||
String modelId = file1.getName();
|
||||
|
||||
entityMap.put(modelId, new Entity(modelId));
|
||||
for (File e : file1.listFiles()) {
|
||||
if (e.getName().endsWith(".png")) {
|
||||
textureMap.put(modelId, new Texture(modelId, e.toPath()));
|
||||
}
|
||||
|
||||
if (e.getName().endsWith(".json")) {
|
||||
try {
|
||||
String json = Files.readString(e.toPath());
|
||||
if (isAnimationFile(json)) {
|
||||
Animation animation = new Animation();
|
||||
animation.load(json);
|
||||
animation.setModelId(modelId);
|
||||
animationMap.put(modelId, animation);
|
||||
}
|
||||
|
||||
if (isGeometryFile(json)) {
|
||||
System.out.println("G");
|
||||
Geometry geometry = new Geometry();
|
||||
geometry.load(json);
|
||||
geometry.setModelId(modelId);
|
||||
geometryMap.put(modelId, geometry);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File animationsFolder = new File(output, "animations");
|
||||
File entityFolder = new File(output, "entity");
|
||||
File modelsFolder = new File(output, "models/entity");
|
||||
File texturesFolder = new File(output, "textures/entity");
|
||||
|
||||
|
||||
boolean generateManifest = false;
|
||||
if (!entityFolder.exists()) {
|
||||
generateManifest = true;
|
||||
}
|
||||
File[] files = entityFolder.listFiles();
|
||||
if (files == null || files.length < entityMap.size()) {
|
||||
generateManifest = true;
|
||||
}
|
||||
|
||||
if (generateManifest) {
|
||||
output.mkdirs();
|
||||
Path path = new File(output, "manifest.json").toPath();
|
||||
try {
|
||||
Files.writeString(path,
|
||||
PackManifest.generate(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
animationsFolder.mkdirs();
|
||||
entityFolder.mkdirs();
|
||||
modelsFolder.mkdirs();
|
||||
texturesFolder.mkdirs();
|
||||
|
||||
for (Map.Entry<String, Animation> stringAnimationEntry : animationMap.entrySet()) {
|
||||
stringAnimationEntry.getValue().modify();
|
||||
Geometry geo = geometryMap.get(stringAnimationEntry.getKey());
|
||||
if (geo != null) {
|
||||
stringAnimationEntry.getValue().addHeadBind(geo);
|
||||
}
|
||||
Path path = animationsFolder.toPath().resolve(stringAnimationEntry.getKey() + ".animation.json");
|
||||
try {
|
||||
Files.writeString(path, GSON.toJson(stringAnimationEntry.getValue().getJson()), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Geometry> stringGeometryEntry : geometryMap.entrySet()) {
|
||||
stringGeometryEntry.getValue().modify();
|
||||
Path path = modelsFolder.toPath().resolve(stringGeometryEntry.getKey() + ".geo.json");
|
||||
try {
|
||||
Files.writeString(path, GSON.toJson(stringGeometryEntry.getValue().getJson()), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Texture> stringTextureEntry : textureMap.entrySet()) {
|
||||
Path path = texturesFolder.toPath().resolve(stringTextureEntry.getKey() + ".png");
|
||||
try {
|
||||
Files.copy(stringTextureEntry.getValue().getPath(), path, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Entity> stringEntityEntry : entityMap.entrySet()) {
|
||||
stringEntityEntry.getValue().modify();
|
||||
Path path = entityFolder.toPath().resolve(stringEntityEntry.getKey() + ".entity.json");
|
||||
|
||||
try {
|
||||
Files.writeString(path, stringEntityEntry.getValue().getJson(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static boolean isGeometryFile(String json) {
|
||||
try {
|
||||
return new JsonParser().parse(json).getAsJsonObject().has("minecraft:geometry");
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAnimationFile(String json) {
|
||||
try {
|
||||
return new JsonParser().parse(json).getAsJsonObject().has("animations");
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package re.imc.geysermodelenginepackgenerator.generator;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import re.imc.geysermodelenginepackgenerator.GeneratorMain;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Animation {
|
||||
|
||||
public static final String HEAD_TEMPLATE = """
|
||||
{
|
||||
"relative_to" : {
|
||||
"rotation" : "entity"
|
||||
},
|
||||
"rotation" : [ "query.target_x_rotation - this", "query.target_y_rotation - this", 0.0 ]
|
||||
}
|
||||
""";
|
||||
|
||||
String modelId;
|
||||
JsonObject json;
|
||||
|
||||
public void load(String json) {
|
||||
this.json = new JsonParser().parse(json).getAsJsonObject();
|
||||
}
|
||||
|
||||
public void modify() {
|
||||
JsonObject newAnimations = new JsonObject();
|
||||
for (Map.Entry<String, JsonElement> element : json.get("animations").getAsJsonObject().entrySet()) {
|
||||
newAnimations.add("animation." + modelId + "." + element.getKey(), element.getValue());
|
||||
}
|
||||
json.add("animations", newAnimations);
|
||||
|
||||
}
|
||||
|
||||
public void addHeadBind(Geometry geometry) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("loop", true);
|
||||
JsonObject bones = new JsonObject();
|
||||
JsonArray array = geometry.getInternal().get("bones").getAsJsonArray();
|
||||
int i = 0;
|
||||
for (JsonElement element : array) {
|
||||
if (element.isJsonObject()) {
|
||||
String name = element.getAsJsonObject().get("name").getAsString();
|
||||
|
||||
String parent = "";
|
||||
if (element.getAsJsonObject().has("parent")) {
|
||||
parent = element.getAsJsonObject().get("parent").getAsString();
|
||||
}
|
||||
if (parent.startsWith("h_") || parent.startsWith("hi_")) {
|
||||
continue;
|
||||
}
|
||||
if (name.startsWith("h_") || name.startsWith("hi_")) {
|
||||
bones.add(name, new JsonParser().parse(HEAD_TEMPLATE));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (i == 0) {
|
||||
return;
|
||||
}
|
||||
GeneratorMain.entityMap
|
||||
.get(modelId).setHasHeadAnimation(true);
|
||||
object.add("bones", bones);
|
||||
json.get("animations").getAsJsonObject().add("animation." + modelId + ".look_at_target", object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package re.imc.geysermodelenginepackgenerator.generator;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Entity {
|
||||
public static final String TEMPLATE = """
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "modelengine:%entity_id%",
|
||||
"materials": {
|
||||
"default": "entity_alphatest"
|
||||
},
|
||||
"textures": {
|
||||
"default": "%texture%"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "%geometry%"
|
||||
},
|
||||
"animations": {
|
||||
"default": "animation.%geometry%.idle",
|
||||
"look_at_target": "%look_at_target%"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
"default",
|
||||
"look_at_target"
|
||||
]
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
String modelId;
|
||||
String json;
|
||||
boolean hasHeadAnimation = false;
|
||||
public Entity(String modelId) {
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
public void modify() {
|
||||
json = TEMPLATE.replace("%entity_id%", modelId)
|
||||
.replace("%geometry%", "geometry.modelengine_" + modelId)
|
||||
.replace("%texture%", "textures/entity/" + modelId)
|
||||
.replace("%look_at_target%", hasHeadAnimation ? "animation." + modelId + ".look_at_target" : "animation.common.look_at_target")
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package re.imc.geysermodelenginepackgenerator.generator;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Geometry {
|
||||
|
||||
String modelId;
|
||||
JsonObject json;
|
||||
public void load(String json) {
|
||||
this.json = new JsonParser().parse(json).getAsJsonObject();
|
||||
}
|
||||
public void setId(String id) {
|
||||
getInternal().get("description").getAsJsonObject().addProperty("identifier", id);
|
||||
}
|
||||
|
||||
public JsonObject getInternal() {
|
||||
return json.get("minecraft:geometry").getAsJsonArray().get(0)
|
||||
.getAsJsonObject();
|
||||
}
|
||||
|
||||
public void modify() {
|
||||
|
||||
JsonArray array = getInternal().get("bones").getAsJsonArray();
|
||||
Iterator<JsonElement> iterator = array.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
JsonElement element = iterator.next();
|
||||
if (element.isJsonObject()) {
|
||||
String name = element.getAsJsonObject().get("name").getAsString();
|
||||
if (name.equals("hitbox") ||
|
||||
name.startsWith("p_") ||
|
||||
name.startsWith("b_") ||
|
||||
name.startsWith("ob_")) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
setId("geometry.modelengine_" + modelId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package re.imc.geysermodelenginepackgenerator.generator;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PackManifest {
|
||||
public static final String TEMPLATE = """
|
||||
{
|
||||
"format_version": 1,
|
||||
"header": {
|
||||
"name": "ModelEngine",
|
||||
"description": "ModelEngine For Geyser",
|
||||
"uuid": "%uuid-1%",
|
||||
"version": [0, 0, 1]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"description": "ModelEngine",
|
||||
"uuid": "%uuid-2%",
|
||||
"version": [0, 0, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
public static String generate() {
|
||||
return TEMPLATE.replace("%uuid-1%", UUID.randomUUID().toString())
|
||||
.replace("%uuid-2%", UUID.randomUUID().toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package re.imc.geysermodelenginepackgenerator.generator;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public class Texture {
|
||||
|
||||
String modelId;
|
||||
Path path;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package re.imc.geysermodelenginepackgenerator.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class ZipUtil {
|
||||
public static void compressFolder(File folder, String folderName, ZipOutputStream zipOutputStream) throws IOException {
|
||||
File[] files = folder.listFiles();
|
||||
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
compressFolder(file, folderName + "/" + file.getName(), zipOutputStream);
|
||||
} else {
|
||||
addToZipFile(folderName + "/" + file.getName(), file, zipOutputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addToZipFile(String fileName, File file, ZipOutputStream zipOutputStream) throws IOException {
|
||||
ZipEntry entry = new ZipEntry(fileName);
|
||||
zipOutputStream.putNextEntry(entry);
|
||||
|
||||
try (FileInputStream fileInputStream = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
|
||||
zipOutputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
6
src/main/resources/extension.yml
Normal file
6
src/main/resources/extension.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
name: GeyserModelEnginePackGenerator
|
||||
id: geysermodelenginepackgenerator
|
||||
main: re.imc.geysermodelenginepackgenerator.ExtensionMain
|
||||
api: 1.0.0
|
||||
version: 1.0.0
|
||||
authors: [zimzaza4]
|
||||
Reference in New Issue
Block a user