The first commit, the core and all files is adding to repo

This commit is contained in:
2026-10-04 14:20:13 +05:00
commit 53d7b263c4
94 changed files with 10668 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
package dev.minegeo.fabric;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.*;
import net.minecraft.client.option.Perspective;
import net.minecraft.client.util.ScreenshotRecorder;
import net.minecraft.util.math.Vec3d;
import java.nio.file.*;
import java.util.concurrent.atomic.AtomicBoolean;
/** Changes the render camera only: no movement or rotation packets are sent to a server. */
public final class Capture {
public static final int SIZE=1024;
public static final String[] FACES={"px","nx","py","ny","pz","nz"};
private static final float[] YAW={-90,90,0,0,0,180},PITCH={0,0,-90,90,0,0};
private static boolean active;private static Framebuffer buffer;private static Path output;
private static Vec3d position;private static Object world;private static int face,settle;
private static boolean hud,bobbing;private static Perspective perspective;
private static final AtomicBoolean pending=new AtomicBoolean();
public static boolean active(){return active;}public static Framebuffer framebuffer(){return buffer;}
public static float yaw(){return YAW[Math.min(face,5)];}public static float pitch(){return PITCH[Math.min(face,5)];}
public static void start(Path directory)throws Exception {
var c=MinecraftClient.getInstance();if(active)throw new IllegalStateException("Capture already running");
if(c.player==null||c.world==null||!c.world.getRegistryKey().getValue().toString().equals("minecraft:overworld"))throw new IllegalStateException("Join an Overworld first");
if(c.player.isSubmergedInWater())throw new IllegalStateException("Capture from a dry surface location");
output=directory;Files.createDirectories(output);position=new Vec3d(c.player.getX(),c.player.getY(),c.player.getZ());world=c.world;
hud=c.options.hudHidden;bobbing=c.options.getBobView().getValue();perspective=c.options.getPerspective();
try {buffer=new SimpleFramebuffer("MineGeo capture",SIZE,SIZE,true);face=0;settle=0;pending.set(false);active=true;c.options.hudHidden=true;c.options.getBobView().setValue(false);c.options.setPerspective(Perspective.FIRST_PERSON);c.gameRenderer.onResized(SIZE,SIZE);}
catch(Throwable e){cancel("Capture setup failed: "+e.getMessage());throw e;}
}
public static void beforeFrame(){if(!active)return;var c=MinecraftClient.getInstance();if(c.player==null||c.world!=world||c.currentScreen!=null||new Vec3d(c.player.getX(),c.player.getY(),c.player.getZ()).squaredDistanceTo(position)>.0004)cancel("Capture cancelled. Stand still until all six views are saved.");}
public static void afterFrame(){if(!active||pending.get()||++settle<4)return;pending.set(true);final int index=face;final Path directory=output;
try{ScreenshotRecorder.takeScreenshot(buffer,image->{try(image){if(active&&output.equals(directory)){image.writeTo(directory.resolve(FACES[index]+".png"));var c=MinecraftClient.getInstance();c.execute(()->{if(!active||!output.equals(directory))return;face++;settle=0;pending.set(false);if(face==6){restore();MineGeoClient.INSTANCE.savedCapture(directory);}});}}catch(Exception e){MinecraftClient.getInstance().execute(()->cancel("Capture failed: "+e.getMessage()));}});}
catch(Exception e){cancel("Capture failed: "+e.getMessage());}
}
public static void cancel(String reason){if(active||buffer!=null){restore();MineGeoClient.INSTANCE.message(reason);}}
private static void restore(){var c=MinecraftClient.getInstance();active=false;pending.set(false);c.options.hudHidden=hud;c.options.getBobView().setValue(bobbing);if(perspective!=null)c.options.setPerspective(perspective);c.gameRenderer.onResized(c.getWindow().getFramebufferWidth(),c.getWindow().getFramebufferHeight());if(buffer!=null){buffer.delete();buffer=null;}}
}

View File

@@ -0,0 +1,17 @@
package dev.minegeo.fabric;
import net.minecraft.client.MinecraftClient;
import java.nio.file.*;
/** Opt-in integration harness, restricted to the disposable loopback test server. */
public final class CaptureFixture {
private static int ticks,completed,wait=160;private static boolean started,cancelTest;
public static void tick(MinecraftClient c){if(!Boolean.getBoolean("minegeo.captureTest")||c.player==null||c.world==null||c.currentScreen!=null)return;
if(c.getCurrentServerEntry()==null||!c.getCurrentServerEntry().address.equals("127.0.0.1:25579"))throw new IllegalStateException("Capture test is restricted to the loopback fixture");
var mod=MineGeoClient.INSTANCE;
try{
if(!started){started=true;mod.dataset=SurfaceDataset.create(mod.server());System.out.println("MINEGEO_TEST_READY");}
if(Capture.active()){if(cancelTest){Capture.cancel("Integration cancellation test");if(c.options.hudHidden)throw new IllegalStateException("HUD was not restored after cancellation");Files.writeString(c.runDirectory.toPath().resolve("capture-test-result.json"),"{\"captures\":5,\"cameraRestored\":true,\"cancelRestored\":true}");System.out.println("MINEGEO_TEST_PASS "+mod.dataset.directory);c.scheduleStop();}return;}
if(mod.dataset.points.size()>completed){if(c.options.hudHidden)throw new IllegalStateException("HUD was not restored after capture");completed=mod.dataset.points.size();System.out.println("MINEGEO_TEST_CAPTURE_"+completed);ticks=0;wait=60;}
if(++ticks<wait)return;ticks=0;if(completed>=5)cancelTest=true;mod.capture();
}catch(Exception e){System.err.println("MINEGEO_TEST_FAIL "+e.getMessage());Capture.cancel("Test failed");c.scheduleStop();}
}
}

View File

@@ -0,0 +1,22 @@
package dev.minegeo.fabric;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.widget.*;
import net.minecraft.text.Text;
public final class CaptureScreen extends Screen {
private TextFieldWidget website,name;
public CaptureScreen(){super(Text.literal("MineGeo Capture"));}
@Override protected void init(){var mod=MineGeoClient.INSTANCE;int x=width/2-130;
website=new TextFieldWidget(textRenderer,x,50,260,20,Text.literal("MineGeo website"));website.setMaxLength(200);website.setText(mod.website);addDrawableChild(website);
name=new TextFieldWidget(textRenderer,x,92,260,20,Text.literal("Map name"));name.setMaxLength(80);name.setText(mod.dataset==null?"My Minecraft map":mod.dataset.name);addDrawableChild(name);
addDrawableChild(ButtonWidget.builder(Text.literal("Capture panorama"),b->{mod.website=website.getText();mod.capture();}).dimensions(x,126,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Preview files"),b->{if(mod.dataset!=null)net.minecraft.util.Util.getOperatingSystem().open(mod.dataset.directory.toFile());}).dimensions(x+134,126,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Link account"),b->{mod.website=website.getText();mod.pair();}).dimensions(x,154,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Upload / resume"),b->{mod.website=website.getText();mod.upload(name.getText());}).dimensions(x+134,154,126,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("New map at this location"),b->{mod.newMap();init(width,height);}).dimensions(x,182,260,20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Close"),b->close()).dimensions(x,210,260,20).build());
}
@Override public void render(DrawContext context,int mouseX,int mouseY,float delta){super.render(context,mouseX,mouseY,delta);int x=width/2-130;context.drawCenteredTextWithShadow(textRenderer,title,width/2,20,0xff8cdf6b);context.drawTextWithShadow(textRenderer,"Website (HTTPS outside localhost)",x,38,0xffffffff);context.drawTextWithShadow(textRenderer,"Map name",x,80,0xffffffff);var mod=MineGeoClient.INSTANCE;String counts=mod.dataset==null?"Join an Overworld to begin":mod.dataset.points.size()+" panoramas · "+mod.dataset.coverage.size()+" explored chunks";context.drawCenteredTextWithShadow(textRenderer,Text.literal(counts),width/2,242,0xffffffff);int y=264;for(var line:textRenderer.wrapLines(Text.literal(mod.status),260)){context.drawTextWithShadow(textRenderer,line,x,y,0xffd5d5d5);y+=10;}}
@Override public boolean shouldPause(){return false;}
}

View File

@@ -0,0 +1,35 @@
package dev.minegeo.fabric;
import dev.minegeo.plugin.UploadClient;
import com.google.gson.*;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.minecraft.util.Identifier;
import net.minecraft.text.Text;
import org.lwjgl.glfw.GLFW;
import java.nio.file.*;
import java.util.concurrent.*;
public final class MineGeoClient implements ClientModInitializer {
public static MineGeoClient INSTANCE;
public final ExecutorService background=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"MineGeo upload");t.setDaemon(true);return t;});
public final Gson gson=new GsonBuilder().setPrettyPrinting().create();
public String website="http://localhost:3000",token="",status="Explore normally to collect surface coverage.";
public volatile boolean uploading=false;public SurfaceDataset dataset;private SurfaceDataset.Point pending;
private Path config;
@Override public void onInitializeClient(){INSTANCE=this;config=MinecraftClient.getInstance().runDirectory.toPath().resolve("config/minegeo.json");try{if(Files.exists(config)){var j=JsonParser.parseString(Files.readString(config)).getAsJsonObject();website=j.get("website").getAsString();token=j.get("token").getAsString();}}catch(Exception e){message("Could not read MineGeo configuration");}
var key=KeyBindingHelper.registerKeyBinding(new KeyBinding("key.minegeo.open",InputUtil.Type.KEYSYM,GLFW.GLFW_KEY_G,KeyBinding.Category.create(Identifier.of("minegeo","capture"))));
ClientTickEvents.END_CLIENT_TICK.register(c->{while(key.wasPressed()){if(Capture.active())Capture.cancel("Capture cancelled");else c.setScreen(new CaptureScreen());}if(c.world!=null&&c.player!=null&&c.world.getRegistryKey().getValue().toString().equals("minecraft:overworld")){try{if(dataset==null||!dataset.server.equals(server()))dataset=SurfaceDataset.open(server());if(!Capture.active()&&!uploading)dataset.scan(c);}catch(Exception e){message("Map collection: "+e.getMessage());}}CaptureFixture.tick(c);});
}
public String server(){var c=MinecraftClient.getInstance();return c.getCurrentServerEntry()!=null?c.getCurrentServerEntry().address:"Singleplayer";}
public void saveConfig()throws Exception{Files.createDirectories(config.getParent());var j=new JsonObject();j.addProperty("website",website);j.addProperty("token",token);Files.writeString(config,gson.toJson(j));}
public void message(String text){status=text;var c=MinecraftClient.getInstance();if(c.player!=null)c.player.sendMessage(Text.literal("[MineGeo] "+text),false);}
public void capture(){try{if(dataset==null)throw new IllegalStateException("Join an Overworld and explore first");var c=MinecraftClient.getInstance();if(!dataset.contains(c.player.getX(),c.player.getZ()))throw new IllegalStateException("Outside this map's 512-block boundary. Start a new map here.");if(dataset.points.size()>=100)throw new IllegalStateException("This map already contains 100 locations");pending=new SurfaceDataset.Point("p"+java.util.UUID.randomUUID().toString().replace("-","").substring(0,12),c.player.getX(),c.player.getEyeY(),c.player.getZ());c.setScreen(null);Capture.start(dataset.directory.resolve(pending.id()));message("Capturing six views. Stand still; press G to cancel.");}catch(Exception e){message(e.getMessage());}}
public void savedCapture(Path path){try{if(pending==null||!path.getFileName().toString().equals(pending.id()))return;dataset.points.add(pending);dataset.save();message("Panorama saved locally ("+dataset.points.size()+" total). Press G for preview or upload.");}catch(Exception e){message("Saving capture failed: "+e.getMessage());}finally{pending=null;}}
public void newMap(){try{if(uploading||Capture.active())throw new IllegalStateException("Finish the current operation first");dataset=SurfaceDataset.create(server());message("New 512 × 512 map started. Older maps remain in .minecraft/minegeo.");}catch(Exception e){message(e.getMessage());}}
public void pair(){background.execute(()->{try{saveConfig();var uploader=new UploadClient(website,"");var pair=uploader.beginPairing();String code=pair.get("deviceCode").getAsString();String link=pair.get("verificationUri").getAsString();MinecraftClient.getInstance().execute(()->{message("Approve code "+pair.get("userCode").getAsString()+" at "+link);net.minecraft.util.Util.getOperatingSystem().open(link);});for(int i=0;i<120;i++){Thread.sleep(5000);var response=uploader.pollPairing(code);if(response.get("status").getAsString().equals("approved")){token=response.get("token").getAsString();saveConfig();MinecraftClient.getInstance().execute(()->message("Account linked. You can upload maps now."));return;}}throw new IllegalStateException("Pairing expired. Try linking again.");}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Pairing: "+e.getMessage()));}});}
public void upload(String name){if(uploading)return;final SurfaceDataset selected=dataset;if(selected==null||selected.points.size()<5){message("Capture at least five locations before uploading.");return;}uploading=true;background.execute(()->{try{selected.name=name;selected.save();saveConfig();status="Uploading assets. Interrupted transfers resume when you retry.";new UploadClient(website,token).upload(selected.directory);MinecraftClient.getInstance().execute(()->message("Upload received. Preview and submit the private draft in your dashboard."));}catch(Exception e){MinecraftClient.getInstance().execute(()->message("Upload paused: "+e.getMessage()));}finally{uploading=false;}});}
}

View File

@@ -0,0 +1,34 @@
package dev.minegeo.fabric;
import com.google.gson.*;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.Heightmap;
import net.minecraft.world.chunk.ChunkStatus;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
/** Reads only chunks already delivered to the client; never requests or generates chunks. */
public final class SurfaceDataset {
public record Point(String id,double x,double y,double z){}
public final Path directory;public final String server;public String name="My Minecraft map";
public final List<Point> points=new ArrayList<>();public final Set<String> coverage=new HashSet<>();
private final BufferedImage image=new BufferedImage(512,512,BufferedImage.TYPE_INT_ARGB);
private int minX,minZ,cursor;private String scanning;private int chunkX,chunkZ;
private SurfaceDataset(Path directory,String server,int x,int z){this.directory=directory;this.server=server;minX=x;minZ=z;for(int iz=0;iz<512;iz++)for(int ix=0;ix<512;ix++)image.setRGB(ix,iz,((ix/16+iz/16)%2==0)?0xff4c493d:0xff3b3932);}
public static SurfaceDataset create(String server)throws Exception{var c=MinecraftClient.getInstance();var root=c.runDirectory.toPath().resolve("minegeo");Files.createDirectories(root);var dataset=new SurfaceDataset(root.resolve(UUID.randomUUID().toString()),server,(Math.floorDiv(c.player.getBlockX(),16)-16)*16,(Math.floorDiv(c.player.getBlockZ(),16)-16)*16);Files.createDirectories(dataset.directory);dataset.saveLocal();return dataset;}
public static SurfaceDataset open(String server)throws Exception{Path root=MinecraftClient.getInstance().runDirectory.toPath().resolve("minegeo");if(Files.exists(root)){try(var dirs=Files.list(root)){for(Path path:dirs.sorted(Comparator.reverseOrder()).toList()){Path state=path.resolve("local.json");if(!Files.exists(state))continue;var j=JsonParser.parseString(Files.readString(state)).getAsJsonObject();if(!j.get("server").getAsString().equals(server))continue;var d=new SurfaceDataset(path,server,j.get("minX").getAsInt(),j.get("minZ").getAsInt());d.name=j.get("name").getAsString();for(var p:j.getAsJsonArray("viewpoints")){var v=p.getAsJsonObject();d.points.add(new Point(v.get("id").getAsString(),v.get("x").getAsDouble(),v.get("y").getAsDouble(),v.get("z").getAsDouble()));}for(var k:j.getAsJsonArray("coverage"))d.coverage.add(k.getAsString());if(Files.exists(path.resolve("map.png"))){var saved=ImageIO.read(path.resolve("map.png").toFile());d.image.getGraphics().drawImage(saved,0,0,null);}return d;}}}return create(server);}
public boolean contains(double x,double z){return x>=minX&&x<minX+512&&z>=minZ&&z<minZ+512;}
public void scan(MinecraftClient c){
if(scanning==null){int px=Math.floorDiv(c.player.getBlockX(),16),pz=Math.floorDiv(c.player.getBlockZ(),16);outer:for(int radius=0;radius<=12;radius++)for(int dz=-radius;dz<=radius;dz++)for(int dx=-radius;dx<=radius;dx++){if(Math.max(Math.abs(dx),Math.abs(dz))!=radius)continue;int cx=px+dx,cz=pz+dz;String key=cx+","+cz;if(coverage.contains(key)||!contains(cx*16,cz*16))continue;if(c.world.getChunk(cx,cz,ChunkStatus.FULL,false)==null)continue;scanning=key;chunkX=cx;chunkZ=cz;cursor=0;break outer;}}
if(scanning==null)return;if(c.world.getChunk(chunkX,chunkZ,ChunkStatus.FULL,false)==null){scanning=null;return;}
var pos=new BlockPos.Mutable();for(int n=0;n<64&&cursor<256;n++,cursor++){int x=chunkX*16+(cursor%16),z=chunkZ*16+(cursor/16),y=c.world.getTopY(Heightmap.Type.WORLD_SURFACE,x,z)-1;pos.set(x,y,z);int color=c.world.getBlockState(pos).getMapColor(c.world,pos).color;image.setRGB(x-minX,z-minZ,0xff000000|color);}
if(cursor==256){coverage.add(scanning);scanning=null;}
}
private JsonObject state(){var g=MineGeoClient.INSTANCE.gson;var j=new JsonObject();j.addProperty("server",server);j.addProperty("name",name);j.addProperty("minX",minX);j.addProperty("minZ",minZ);j.add("viewpoints",g.toJsonTree(points));j.add("coverage",g.toJsonTree(coverage));return j;}
private void saveLocal()throws Exception{Files.createDirectories(directory);Files.writeString(directory.resolve("local.json"),MineGeoClient.INSTANCE.gson.toJson(state()));ImageIO.write(image,"png",directory.resolve("map.png").toFile());}
public void save()throws Exception{saveLocal();var j=new JsonObject();j.addProperty("format","minegeo-dataset/v1");j.addProperty("minecraftVersion","1.21.11");j.addProperty("dimension","minecraft:overworld");j.addProperty("north","-z");j.addProperty("source","fabric");j.addProperty("name",name);j.addProperty("description","Player-captured surface map. Checkerboard areas have not been explored.");j.addProperty("serverName",server);if(!server.equals("Singleplayer"))j.addProperty("serverAddress",server);var b=new JsonObject();b.addProperty("minX",minX);b.addProperty("maxX",minX+512);b.addProperty("minZ",minZ);b.addProperty("maxZ",minZ+512);j.add("bounds",b);j.add("viewpoints",MineGeoClient.INSTANCE.gson.toJsonTree(points));var chunks=new JsonArray();for(String key:coverage){var v=new JsonArray();for(String number:key.split(","))v.add(Integer.parseInt(number));chunks.add(v);}j.add("coverage",chunks);var assets=new JsonArray();addAsset(assets,"map.png");for(Point p:points)for(String face:Capture.FACES)addAsset(assets,p.id()+"/"+face+".png");j.add("assets",assets);Files.writeString(directory.resolve("manifest.json"),MineGeoClient.INSTANCE.gson.toJson(j));}
private void addAsset(JsonArray array,String file)throws Exception{Path p=directory.resolve(file);var j=new JsonObject();j.addProperty("path",file);j.addProperty("bytes",Files.size(p));j.addProperty("sha256",HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(p))));array.add(j);}
}

View File

@@ -0,0 +1,12 @@
package dev.minegeo.fabric.mixin;
import dev.minegeo.fabric.Capture;
import net.minecraft.client.render.Camera;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow protected abstract void setRotation(float yaw,float pitch);
@Inject(method="update",at=@At("TAIL")) private void direction(CallbackInfo ci){if(Capture.active())setRotation(Capture.yaw(),Capture.pitch());}
}

View File

@@ -0,0 +1,15 @@
package dev.minegeo.fabric.mixin;
import dev.minegeo.fabric.Capture;
import net.minecraft.client.render.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Inject(method="render",at=@At("HEAD")) private void before(CallbackInfo ci){Capture.beforeFrame();}
@Inject(method="render",at=@At("RETURN")) private void after(CallbackInfo ci){Capture.afterFrame();}
@Inject(method="getFov",at=@At("HEAD"),cancellable=true) private void fov(CallbackInfoReturnable<Float> ci){if(Capture.active())ci.setReturnValue(90f);}
@Inject(method={"renderHand","bobView","tiltViewWhenHurt"},at=@At("HEAD"),cancellable=true) private void clean(CallbackInfo ci){if(Capture.active())ci.cancel();}
}

View File

@@ -0,0 +1,11 @@
package dev.minegeo.fabric.mixin;
import dev.minegeo.fabric.Capture;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.Framebuffer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(MinecraftClient.class)
public class MinecraftClientMixin {
@Inject(method="getFramebuffer",at=@At("HEAD"),cancellable=true) private void square(CallbackInfoReturnable<Framebuffer> ci){if(Capture.active()&&Capture.framebuffer()!=null)ci.setReturnValue(Capture.framebuffer());}
}

View File

@@ -0,0 +1,10 @@
package dev.minegeo.fabric.mixin;
import dev.minegeo.fabric.Capture;
import net.minecraft.client.util.Window;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Window.class)
public class WindowMixin {
@Inject(method={"getFramebufferWidth","getFramebufferHeight"},at=@At("HEAD"),cancellable=true) private void square(CallbackInfoReturnable<Integer> ci){if(Capture.active())ci.setReturnValue(Capture.SIZE);}
}

View File

@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "minegeo",
"version": "${version}",
"name": "MineGeo Capture",
"description": "Capture six-face Minecraft panoramas and surface maps, then upload them to MineGeo.",
"environment": "client",
"entrypoints": {"client": ["dev.minegeo.fabric.MineGeoClient"]},
"mixins": ["minegeo.mixins.json"],
"depends": {"fabricloader": ">=0.19.5", "minecraft": "1.21.11", "java": ">=21", "fabric-api": "*"}
}

View File

@@ -0,0 +1 @@
{"required":true,"package":"dev.minegeo.fabric.mixin","compatibilityLevel":"JAVA_21","client":["GameRendererMixin","WindowMixin","MinecraftClientMixin","CameraMixin"],"injectors":{"defaultRequire":1}}