27 Commits

Author SHA1 Message Date
a6f8645c68 Added toggleable scrollToZoom
All checks were successful
build / build (push) Successful in 1m13s
2025-05-20 12:01:59 +02:00
2be7c16e8b Refactored SFKeyBindings to CameraShortcut 2025-05-20 11:59:51 +02:00
ccbb15f7c4 Fixed bug in Replenish feature
All checks were successful
build / build (push) Successful in 1m43s
2025-05-19 22:23:08 +02:00
05a93b5ab0 Updated README.md
All checks were successful
build / build (push) Successful in 1m14s
2025-05-19 20:36:47 +02:00
327c92a81c Updated README.md 2025-05-19 20:32:24 +02:00
29067365ca Added Zoom functionality 2025-05-19 20:32:00 +02:00
1ae193ada7 Greatly improved Vein Miner performance 2025-05-19 20:31:13 +02:00
151fd022c4 Cleaned up code
All checks were successful
build / build (push) Successful in 1m25s
2025-05-19 12:06:39 +02:00
deb338f2c8 Added configurable Vein Miner animation 2025-05-19 11:59:39 +02:00
61d2a0b137 Updated README.md 2025-05-19 10:20:58 +02:00
35148c2159 Added Replenish function (replanting crops) 2025-05-19 10:20:47 +02:00
ef29652e07 Made leaves veinmineable using shears 2025-05-19 10:15:29 +02:00
ee448ae34d Removed debug message
All checks were successful
build / build (push) Successful in 1m11s
2025-04-27 16:36:36 +02:00
69df0e5850 Cleaned up some code 2025-04-27 15:41:38 +02:00
4988a91e70 Fixed mixin class types 2025-04-27 15:37:14 +02:00
8808df1cf1 Added zoom functionality
All checks were successful
build / build (push) Successful in 1m39s
2025-04-23 22:32:18 +02:00
963002570d Added zoom functionality 2025-04-23 22:32:13 +02:00
2c95b5e374 Oops 2025-04-23 21:26:56 +02:00
1d3dba1b02 Changed Mixin from interface(?) to abstract class 2025-04-23 21:21:10 +02:00
c7ea933494 Added documentation to Mixin classes 2025-04-23 20:53:15 +02:00
be6472cc79 Updated README.md
All checks were successful
build / build (push) Successful in 1m19s
2025-04-22 16:34:35 +02:00
49ee0c503f Fixed Chat Calc 2025-04-22 16:34:22 +02:00
f994e8cc11 Changed default config 2025-04-22 16:33:58 +02:00
8498a77735 Fixed toggleability for Vein Miner
All checks were successful
build / build (push) Successful in 1m14s
2025-04-13 17:51:47 +02:00
0dd5907a29 Updated mod version 2025-04-12 23:49:27 +02:00
c25790902c Revamped Config Screen 2025-04-12 23:48:47 +02:00
f51bde3d6e Render Block Entities from further away 2025-04-12 20:53:52 +02:00
27 changed files with 1054 additions and 296 deletions

View File

@ -8,7 +8,6 @@ As a challenge I'm trying to make it as user-friendly as possible.
## Server Side ## Server Side
### Features
- Custom join message - Custom join message
![Join Message](https://i.imgur.com/7uv5lUb.png) ![Join Message](https://i.imgur.com/7uv5lUb.png)
- Custom quit message - Custom quit message
@ -29,6 +28,7 @@ As a challenge I'm trying to make it as user-friendly as possible.
- Open/close EC block while opening/closing SEC - Open/close EC block while opening/closing SEC
- Play open & close sounds - Play open & close sounds
- Vein miner - Vein miner
Code inspired by Inferis!
![VeinMiner](https://i.imgur.com/zOXWMNa.gif) ![VeinMiner](https://i.imgur.com/zOXWMNa.gif)
- Chat Calculator - Chat Calculator
@ -47,14 +47,46 @@ As a challenge I'm trying to make it as user-friendly as possible.
- Automatic config adaption (currently booleans only) - Automatic config adaption (currently booleans only)
- Remove darkness effect - Remove darkness effect
- Toggleable - Toggleable
- Keybinding for /camera - Keybinding for /spectator
# Currently working on
## Server Side
- [x] Chat Calculator
- [x] Check if operator is present before calculating
- Vein Miner
- [x] Configurable animation (tick delay)
- [x] Leaves veinmineable using shears
- [x] Replenish
## Client Side
- [x] Render block entities from a longer range
- [x] Toggleable via GUI
- [x] Configurable value
- [x] In GUI
- [x] Rework Mod Menu integration to be more flexible
DISCLAIMER: this is NOT perfect and still needs to be reworked, I'm just too lazy right now...
- The following types are accepted:
- String
- Boolean
- Float
- Integer
- [x] Zoom
- [x] Configurable
- Smooth zoom
- Initial zoom value
- Zoom step value
- Scroll to zoom further
# To-do # To-do
## General ## General
- Rework config system - Rework config system
- Store server settings in world folder for better singleplayer use - Store server settings in world folder for better singleplayer use
- Rework Mod Menu integration to be more flexible
## Client side
## Server Side ## Server Side
- Telekinesis - Telekinesis

View File

@ -9,7 +9,7 @@ yarn_mappings=1.21.5+build.1
loader_version=0.16.10 loader_version=0.16.10
# Mod Properties # Mod Properties
mod_version=1.4.0 mod_version=1.4.1
maven_group=wtf.hak.survivalfabric maven_group=wtf.hak.survivalfabric
archives_base_name=survivalfabric archives_base_name=survivalfabric

View File

@ -4,7 +4,8 @@ import net.fabricmc.api.ClientModInitializer;
import wtf.hak.survivalfabric.config.client.ClientConfigManager; import wtf.hak.survivalfabric.config.client.ClientConfigManager;
import wtf.hak.survivalfabric.features.AngleViewer; import wtf.hak.survivalfabric.features.AngleViewer;
import wtf.hak.survivalfabric.features.RemoveDarknessEffect; import wtf.hak.survivalfabric.features.RemoveDarknessEffect;
import wtf.hak.survivalfabric.features.SFKeyBindings; import wtf.hak.survivalfabric.features.CameraShortcut;
import wtf.hak.survivalfabric.features.Zoom;
public class SurvivalFabricClient implements ClientModInitializer { public class SurvivalFabricClient implements ClientModInitializer {
@ -17,7 +18,7 @@ public class SurvivalFabricClient implements ClientModInitializer {
// Features // Features
AngleViewer.register(); AngleViewer.register();
RemoveDarknessEffect.register(); RemoveDarknessEffect.register();
SFKeyBindings.register(); CameraShortcut.register();
Zoom.register();
} }
} }

View File

@ -1,7 +1,7 @@
package wtf.hak.survivalfabric.config.client; package wtf.hak.survivalfabric.config.client;
public class ClientConfig { public class ClientConfig {
public String configVersion = "1.0"; public String configVersion = "1.1";
public boolean renderNetherFog = false; public boolean renderNetherFog = false;
public boolean renderOverworldFog = false; public boolean renderOverworldFog = false;
@ -11,4 +11,10 @@ public class ClientConfig {
public boolean renderSnowFog = false; public boolean renderSnowFog = false;
public boolean removeDarknessEffect = true; public boolean removeDarknessEffect = true;
public boolean lockTeleportHeadMovement = true; public boolean lockTeleportHeadMovement = true;
public boolean manipulateBlockEntityDistance = true;
public int blockEntityRange = 512;
public boolean smoothCamera = true;
public float initialZoom = 20f;
public boolean scrollToZoom = true;
public float zoomStep = 2.5f;
} }

View File

@ -6,13 +6,13 @@ import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil; import net.minecraft.client.util.InputUtil;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
public class SFKeyBindings { public class CameraShortcut {
private static final KeyBinding CAMERA_BIND = KeyBindingHelper.registerKeyBinding(new KeyBinding("key.survivalfabric.camera", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_PERIOD, "category.survivalfabric.survivalfabric")); private static final KeyBinding CAMERA_BIND = KeyBindingHelper.registerKeyBinding(new KeyBinding("key.survivalfabric.camera", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_PERIOD, "category.survivalfabric.survivalfabric"));
public static void register() { public static void register() {
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
if(client.player != null) { if (client.player != null) {
if (CAMERA_BIND.wasPressed()) { if (CAMERA_BIND.wasPressed()) {
client.player.networkHandler.sendChatCommand("camera"); client.player.networkHandler.sendChatCommand("camera");
} }

View File

@ -0,0 +1,63 @@
package wtf.hak.survivalfabric.features;
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.text.Text;
import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Unique;
import static wtf.hak.survivalfabric.config.client.ClientConfigManager.getConfig;
public class Zoom {
private static final KeyBinding ZOOM_BIND = KeyBindingHelper.registerKeyBinding(new KeyBinding("key.survivalfabric.zoom", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_C, "category.survivalfabric.survivalfabric"));
private static boolean SHOULD_ZOOM = false;
private static int ZOOM_STEP = 0;
@Unique
private static boolean initialSmoothZoom;
public static void register() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (ZOOM_BIND.isPressed() && !SHOULD_ZOOM) {
if(getConfig().smoothCamera) {
initialSmoothZoom = MinecraftClient.getInstance().options.smoothCameraEnabled;
MinecraftClient.getInstance().options.smoothCameraEnabled = true;
}
SHOULD_ZOOM = true;
} else if (!ZOOM_BIND.isPressed() && SHOULD_ZOOM) {
SHOULD_ZOOM = false;
ZOOM_STEP = 0;
if(getConfig().smoothCamera) {
MinecraftClient.getInstance().options.smoothCameraEnabled = initialSmoothZoom;
}
}
});
}
public static boolean isZooming() {
return SHOULD_ZOOM;
}
public static float getZoomFov() {
return getConfig().initialZoom - -ZOOM_STEP * getConfig().zoomStep;
}
public static void modifyStep(int step) {
ZOOM_STEP += step;
// Clamp the zoom level so the FOV stays within [1, 110]
float zoomFov = getZoomFov();
if (zoomFov < 1) {
ZOOM_STEP = Math.round((1 - getConfig().initialZoom) / getConfig().zoomStep) + 1;
} else if (zoomFov > 110) {
ZOOM_STEP = Math.round((110 - getConfig().initialZoom) / getConfig().zoomStep);
}
}
}

View File

@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import static wtf.hak.survivalfabric.config.client.ClientConfigManager.getConfig; import static wtf.hak.survivalfabric.config.client.ClientConfigManager.getConfig;
@Mixin(value = BackgroundRenderer.class, priority = 910) @Mixin(value = BackgroundRenderer.class, priority = 910)
public abstract class BackgroundRendererMixin { public class BackgroundRendererMixin {
@Unique @Unique
private static final Fog EMPTY_FOG = new Fog(-8.0f, 1_000_000.0F, FogShape.CYLINDER, 0, 0, 0, 0); private static final Fog EMPTY_FOG = new Fog(-8.0f, 1_000_000.0F, FogShape.CYLINDER, 0, 0, 0, 0);

View File

@ -0,0 +1,19 @@
package wtf.hak.survivalfabric.mixin.client;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.client.render.block.entity.BlockEntityRenderer;
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.CallbackInfoReturnable;
import wtf.hak.survivalfabric.config.client.ClientConfigManager;
@Mixin(BlockEntityRenderer.class)
public interface BlockEntityRendererMixin<T extends BlockEntity> {
@Inject(method = "getRenderDistance", at = @At("HEAD"), cancellable = true)
private void getRenderDistance(CallbackInfoReturnable<Integer> cir) {
if (ClientConfigManager.getConfig().manipulateBlockEntityDistance)
cir.setReturnValue(ClientConfigManager.getConfig().blockEntityRange);
}
}

View File

@ -0,0 +1,21 @@
package wtf.hak.survivalfabric.mixin.client;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.render.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import wtf.hak.survivalfabric.features.Zoom;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
/**
* Modify Zoom FOV
*/
@ModifyReturnValue(method = "getFov", at = @At("RETURN"))
private float modifyFovWithZoom(float fov, @Local(argsOnly = true) float tickDelta) {
return Zoom.isZooming() ? Zoom.getZoomFov() : fov;
}
}

View File

@ -0,0 +1,33 @@
package wtf.hak.survivalfabric.mixin.client;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.Mouse;
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 wtf.hak.survivalfabric.features.Zoom;
import static wtf.hak.survivalfabric.config.client.ClientConfigManager.getConfig;
@Mixin(Mouse.class)
public class MouseMixin {
/**
* Scrolling listener for zooming
*/
@Inject(method = "onMouseScroll", at = @At("HEAD"), cancellable = true)
private void onMouseScroll(long window, double horizontal, double vertical, CallbackInfo ci) {
if (Zoom.isZooming() && getConfig().scrollToZoom) {
if (MinecraftClient.getInstance().player != null) {
if (vertical > 0)
Zoom.modifyStep(-1);
else
Zoom.modifyStep(1);
}
ci.cancel();
}
}
}

View File

@ -3,15 +3,29 @@ package wtf.hak.survivalfabric.modmenu;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.math.MathHelper;
import wtf.hak.survivalfabric.config.client.ClientConfig; import wtf.hak.survivalfabric.config.client.ClientConfig;
import wtf.hak.survivalfabric.config.client.ClientConfigManager; import wtf.hak.survivalfabric.config.client.ClientConfigManager;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ConfigScreen extends Screen { public class ConfigScreen extends Screen {
private static final int OPTION_HEIGHT = 25;
private static final int SCROLL_BAR_WIDTH = 6;
private static final int TOP_PADDING = 40;
private static final int BOTTOM_PADDING = 35;
private static final int SIDE_PADDING = 20;
private final Screen parent; private final Screen parent;
private final List<ConfigOption<?>> options = new ArrayList<>();
private TextFieldWidget activeTextField = null;
private float scrollPosition = 0.0F;
private boolean scrolling = false;
private int contentHeight = 0;
public ConfigScreen(Screen parent) { public ConfigScreen(Screen parent) {
super(Text.literal("Survival Fabric - Client Config")); super(Text.literal("Survival Fabric - Client Config"));
@ -20,55 +34,272 @@ public class ConfigScreen extends Screen {
@Override @Override
protected void init() { protected void init() {
int buttonWidth = 200; options.clear();
int buttonHeight = 20;
int spacing = 5;
int startY = 40;
int i = 0;
int listWidth = this.width - (SIDE_PADDING * 2);
int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING;
int listLeft = SIDE_PADDING;
int listTop = TOP_PADDING;
int index = 0;
for (Field field : ClientConfig.class.getFields()) { for (Field field : ClientConfig.class.getFields()) {
if (field.getType() == boolean.class) { try {
int y = startY + i * (buttonHeight + spacing); Class<?> type = field.getType();
try { String name = formatFieldName(field.getName());
ConfigOption<?> option = null;
if (type == boolean.class) {
boolean value = field.getBoolean(ClientConfigManager.getConfig()); boolean value = field.getBoolean(ClientConfigManager.getConfig());
String label = formatFieldName(field.getName()) + ": " + (value ? "ON" : "OFF"); option = new BooleanConfigOption(
name,
ButtonWidget button = ButtonWidget.builder( field,
Text.literal(label), value,
b -> { listLeft,
try { listTop + (index * OPTION_HEIGHT) - (int) scrollPosition,
boolean current = field.getBoolean(ClientConfigManager.getConfig()); listWidth
field.setBoolean(ClientConfigManager.getConfig(), !current); );
b.setMessage(Text.literal(formatFieldName(field.getName()) + ": " + (!current ? "ON" : "OFF"))); } else if (type == int.class) {
ClientConfigManager.save(); // Save if needed int value = field.getInt(ClientConfigManager.getConfig());
} catch (Exception e) { option = new IntegerConfigOption(
e.printStackTrace(); name,
} field,
} value,
).dimensions(this.width / 2 - buttonWidth / 2, y, buttonWidth, buttonHeight).build(); listLeft,
listTop + (index * OPTION_HEIGHT) - (int) scrollPosition,
this.addDrawableChild(button); listWidth
i++; );
} catch (Exception e) { } else if (type == float.class) {
e.printStackTrace(); float value = field.getFloat(ClientConfigManager.getConfig());
option = new FloatConfigOption(
name,
field,
value,
listLeft,
listTop + (index * OPTION_HEIGHT) - (int) scrollPosition,
listWidth
);
} else if (type == String.class) {
String value = (String) field.get(ClientConfigManager.getConfig());
option = new StringConfigOption(
name,
field,
value,
listLeft,
listTop + (index * OPTION_HEIGHT) - (int) scrollPosition,
listWidth
);
} }
if (option != null) {
options.add(option);
index++;
}
} catch (Exception e) {
e.printStackTrace();
} }
} }
for (ConfigOption<?> option : options) {
if (option instanceof NumericConfigOption) {
((NumericConfigOption<?>) option).createTextField(this.client);
}
}
contentHeight = options.size() * OPTION_HEIGHT;
this.addDrawableChild(ButtonWidget.builder( this.addDrawableChild(ButtonWidget.builder(
Text.translatable("gui.done"), Text.translatable("gui.done"),
button -> this.client.setScreen(parent) button -> this.client.setScreen(parent)
).dimensions(this.width / 2 - 75, startY + i * (buttonHeight + spacing) + 10, 150, 20).build()); ).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build());
} }
@Override @Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) { public void render(DrawContext context, int mouseX, int mouseY, float delta) {
context.fill(0, 0, this.width, this.height, 0xC0101010); this.renderBackground(context, mouseX, mouseY, delta);
int titleX = (this.width / 2) - (this.textRenderer.getWidth(this.title) / 2); int titleX = (this.width / 2) - (this.textRenderer.getWidth(this.title) / 2);
context.drawTextWithShadow(this.textRenderer, this.title, titleX, 20, 0xFFFFFF); context.drawText(this.textRenderer, this.title, titleX, 15, 0xFFFFFF, true);
super.render(context, mouseX, mouseY, delta); int listWidth = this.width - (SIDE_PADDING * 2);
int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING;
int listLeft = SIDE_PADDING;
int listTop = TOP_PADDING;
context.drawBorder(listLeft, listTop, listLeft + listWidth, listTop + listHeight, 0x00000000);
context.enableScissor(
listLeft,
listTop,
listLeft + listWidth,
listTop + listHeight
);
for (int i = 0; i < options.size(); i++) {
ConfigOption<?> option = options.get(i);
option.y = listTop + (i * OPTION_HEIGHT) - (int) scrollPosition;
if (option instanceof NumericConfigOption) {
((NumericConfigOption<?>) option).updateTextFieldPosition();
}
if (option.y < listTop + listHeight && option.y + OPTION_HEIGHT > listTop) {
option.render(context, mouseX, mouseY, delta);
}
}
context.disableScissor();
if (contentHeight > listHeight) {
int scrollBarHeight = Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight));
int scrollBarY = listTop + (int) ((scrollPosition / (contentHeight - listHeight)) * (listHeight - scrollBarHeight));
context.fill(
listLeft + listWidth + 2,
listTop,
listLeft + listWidth + 2 + SCROLL_BAR_WIDTH,
listTop + listHeight,
0xFF404040
);
context.fill(
listLeft + listWidth + 2,
scrollBarY,
listLeft + listWidth + 2 + SCROLL_BAR_WIDTH,
scrollBarY + scrollBarHeight,
scrolling ? 0xFFAAAAAA : 0xFF808080
);
}
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
int listWidth = this.width - (SIDE_PADDING * 2);
int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING;
int listLeft = SIDE_PADDING;
int listTop = TOP_PADDING;
boolean handledByTextField = false;
activeTextField = null;
if (mouseX >= listLeft &&
mouseX <= listLeft + listWidth &&
mouseY >= listTop &&
mouseY <= listTop + listHeight) {
for (ConfigOption<?> option : options) {
if (option instanceof NumericConfigOption<?> numOption &&
option.y >= listTop &&
option.y + OPTION_HEIGHT <= listTop + listHeight) {
TextFieldWidget textField = numOption.getTextField();
if (textField.isMouseOver(mouseX, mouseY)) {
textField.setFocused(true);
activeTextField = textField;
handledByTextField = true;
for (ConfigOption<?> otherOption : options) {
if (otherOption instanceof NumericConfigOption && otherOption != option) {
((NumericConfigOption<?>) otherOption).getTextField().setFocused(false);
}
}
return true;
} else {
textField.setFocused(false);
}
}
}
}
if (contentHeight > listHeight &&
mouseX >= listLeft + listWidth + 2 &&
mouseX <= listLeft + listWidth + 2 + SCROLL_BAR_WIDTH &&
mouseY >= listTop &&
mouseY <= listTop + listHeight) {
scrolling = true;
return true;
}
// Check if clicked on an option
if (mouseX >= listLeft &&
mouseX <= listLeft + listWidth &&
mouseY >= listTop &&
mouseY <= listTop + listHeight) {
for (ConfigOption<?> option : options) {
if (option.isMouseOver(mouseX, mouseY) && option.y >= listTop && option.y + OPTION_HEIGHT <= listTop + listHeight) {
option.onClick(mouseX, mouseY);
return true;
}
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (activeTextField != null && activeTextField.isFocused()) {
return activeTextField.keyPressed(keyCode, scanCode, modifiers);
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean charTyped(char chr, int modifiers) {
if (activeTextField != null && activeTextField.isFocused()) {
return activeTextField.charTyped(chr, modifiers);
}
return super.charTyped(chr, modifiers);
}
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
scrolling = false;
return super.mouseReleased(mouseX, mouseY, button);
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING;
int listTop = TOP_PADDING;
if (scrolling && contentHeight > listHeight) {
float scrollAmount = (float) deltaY / (listHeight - Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight)));
float maxScroll = contentHeight - listHeight;
scrollPosition = MathHelper.clamp(scrollPosition + scrollAmount * maxScroll, 0.0F, maxScroll);
return true;
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int listWidth = this.width - (SIDE_PADDING * 2);
int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING;
int listLeft = SIDE_PADDING;
int listTop = TOP_PADDING;
if (mouseX >= listLeft &&
mouseX <= listLeft + listWidth &&
mouseY >= listTop &&
mouseY <= listTop + listHeight &&
contentHeight > listHeight) {
float maxScroll = contentHeight - listHeight;
scrollPosition = MathHelper.clamp(scrollPosition - (float) verticalAmount * 10, 0.0F, maxScroll);
return true;
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
} }
private String formatFieldName(String rawName) { private String formatFieldName(String rawName) {
@ -84,4 +315,285 @@ public class ConfigScreen extends Screen {
return result.toString(); return result.toString();
} }
} /**
* Base class for all config options
*/
private abstract class ConfigOption<T> {
protected final String name;
protected final Field field;
protected final int width;
protected T value;
protected int x;
protected int y;
public ConfigOption(String name, Field field, T initialValue, int x, int y, int width) {
this.name = name;
this.field = field;
this.value = initialValue;
this.x = x;
this.y = y;
this.width = width;
}
public abstract void render(DrawContext context, int mouseX, int mouseY, float delta);
public abstract void onClick(double mouseX, double mouseY);
public boolean isMouseOver(double mouseX, double mouseY) {
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + OPTION_HEIGHT;
}
protected abstract void saveValue();
}
/**
* Implementation for boolean config options
*/
private class BooleanConfigOption extends ConfigOption<Boolean> {
private static final int BUTTON_WIDTH = 40;
private static final int BUTTON_HEIGHT = 20;
public BooleanConfigOption(String name, Field field, Boolean initialValue, int x, int y, int width) {
super(name, field, initialValue, x, y, width);
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true);
int buttonX = x + width - BUTTON_WIDTH - 5;
int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2;
boolean hovered = isButtonHovered(mouseX, mouseY);
int buttonColor = hovered ? 0xFF404040 : 0xFF303030;
int buttonBorder = hovered ? 0xFFCCCCCC : 0xFF808080;
context.fill(buttonX, buttonY, buttonX + BUTTON_WIDTH, buttonY + BUTTON_HEIGHT, buttonBorder);
context.fill(buttonX + 1, buttonY + 1, buttonX + BUTTON_WIDTH - 1, buttonY + BUTTON_HEIGHT - 1, buttonColor);
String buttonText = value ? "true" : "false";
int textColor = value ? 0x00be00 : 0xbe0000;
int textWidth = textRenderer.getWidth(buttonText);
context.drawText(
textRenderer,
buttonText,
buttonX + (BUTTON_WIDTH - textWidth) / 2,
buttonY + (BUTTON_HEIGHT - 8) / 2,
textColor,
true
);
}
public boolean isButtonHovered(double mouseX, double mouseY) {
int buttonX = x + width - BUTTON_WIDTH - 5;
int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2;
return mouseX >= buttonX && mouseX <= buttonX + BUTTON_WIDTH &&
mouseY >= buttonY && mouseY <= buttonY + BUTTON_HEIGHT;
}
@Override
public void onClick(double mouseX, double mouseY) {
if (isButtonHovered(mouseX, mouseY)) {
value = !value;
saveValue();
}
}
@Override
protected void saveValue() {
try {
// Update config and save
field.setBoolean(ClientConfigManager.getConfig(), value);
ClientConfigManager.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Implementation for String config options
*/
private class StringConfigOption extends ConfigOption<String> {
private static final int FIELD_WIDTH = 60;
private static final int FIELD_HEIGHT = 16;
protected TextFieldWidget textField;
public StringConfigOption(String name, Field field, String initialValue, int x, int y, int width) {
super(name, field, initialValue, x, y, width);
}
public void createTextField(net.minecraft.client.MinecraftClient client) {
int fieldX = x + width - FIELD_WIDTH - 5;
int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2;
textField = new TextFieldWidget(
textRenderer,
fieldX,
fieldY,
FIELD_WIDTH,
FIELD_HEIGHT,
Text.literal("")
);
textField.setText(value);
textField.setMaxLength(10);
textField.setChangedListener(this::onTextChanged);
}
public TextFieldWidget getTextField() {
return textField;
}
public void updateTextFieldPosition() {
if (textField != null) {
int fieldX = x + width - FIELD_WIDTH - 5;
int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2;
textField.setX(fieldX);
textField.setY(fieldY);
}
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true);
if (textField != null) {
textField.render(context, mouseX, mouseY, delta);
}
}
@Override
public void onClick(double mouseX, double mouseY) {
}
public void onTextChanged(String text) {
try {
value = text;
saveValue();
} catch (NumberFormatException e) {}
}
@Override
protected void saveValue() {
try {
field.set(ClientConfigManager.getConfig(), value);
ClientConfigManager.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Base class for numeric config options (int and float)
*/
private abstract class NumericConfigOption<T extends Number> extends ConfigOption<T> {
private static final int FIELD_WIDTH = 60;
private static final int FIELD_HEIGHT = 16;
protected TextFieldWidget textField;
public NumericConfigOption(String name, Field field, T initialValue, int x, int y, int width) {
super(name, field, initialValue, x, y, width);
}
public void createTextField(net.minecraft.client.MinecraftClient client) {
int fieldX = x + width - FIELD_WIDTH - 5;
int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2;
textField = new TextFieldWidget(
textRenderer,
fieldX,
fieldY,
FIELD_WIDTH,
FIELD_HEIGHT,
Text.literal("")
);
textField.setText(value.toString());
textField.setMaxLength(10);
textField.setChangedListener(this::onTextChanged);
}
public TextFieldWidget getTextField() {
return textField;
}
public void updateTextFieldPosition() {
if (textField != null) {
int fieldX = x + width - FIELD_WIDTH - 5;
int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2;
textField.setX(fieldX);
textField.setY(fieldY);
}
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true);
if (textField != null) {
textField.render(context, mouseX, mouseY, delta);
}
}
@Override
public void onClick(double mouseX, double mouseY) {}
protected abstract void onTextChanged(String text);
}
/**
* Implementation for integer config options
*/
private class IntegerConfigOption extends NumericConfigOption<Integer> {
public IntegerConfigOption(String name, Field field, Integer initialValue, int x, int y, int width) {
super(name, field, initialValue, x, y, width);
}
@Override
protected void onTextChanged(String text) {
try {
value = Integer.parseInt(text);
saveValue();
} catch (NumberFormatException e) {}
}
@Override
protected void saveValue() {
try {
field.setInt(ClientConfigManager.getConfig(), value);
ClientConfigManager.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Implementation for float config options
*/
private class FloatConfigOption extends NumericConfigOption<Float> {
public FloatConfigOption(String name, Field field, Float initialValue, int x, int y, int width) {
super(name, field, initialValue, x, y, width);
}
@Override
protected void onTextChanged(String text) {
try {
value = Float.parseFloat(text);
saveValue();
} catch (NumberFormatException e) {}
}
@Override
protected void saveValue() {
try {
field.setFloat(ClientConfigManager.getConfig(), value);
ClientConfigManager.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@ -4,7 +4,10 @@
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"client": [ "client": [
"BackgroundRendererMixin", "BackgroundRendererMixin",
"EntityMixin" "BlockEntityRendererMixin",
"EntityMixin",
"GameRendererMixin",
"MouseMixin"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1

View File

@ -12,6 +12,7 @@ import wtf.hak.survivalfabric.commands.SlimeChunkCommand;
import wtf.hak.survivalfabric.commands.SpectatorCommand; import wtf.hak.survivalfabric.commands.SpectatorCommand;
import wtf.hak.survivalfabric.features.sharedenderchest.SharedEnderChest; import wtf.hak.survivalfabric.features.sharedenderchest.SharedEnderChest;
import wtf.hak.survivalfabric.features.veinminer.VeinMinerEvents; import wtf.hak.survivalfabric.features.veinminer.VeinMinerEvents;
import wtf.hak.survivalfabric.utils.Scheduler;
import static wtf.hak.survivalfabric.config.ConfigManager.getConfig; import static wtf.hak.survivalfabric.config.ConfigManager.getConfig;
@ -23,6 +24,7 @@ public class SurvivalFabric implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
Scheduler.initialize();
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> SpectatorCommand.register(dispatcher, "spectator", "s", "S", "camera", "c", "C")); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> SpectatorCommand.register(dispatcher, "spectator", "s", "S", "camera", "c", "C"));
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> ReloadConfigCommand.register(dispatcher, "reloadsurvivalconfig")); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> ReloadConfigCommand.register(dispatcher, "reloadsurvivalconfig"));
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> SlimeChunkCommand.register(dispatcher, "slimechunk", "sc")); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> SlimeChunkCommand.register(dispatcher, "slimechunk", "sc"));
@ -32,7 +34,7 @@ public class SurvivalFabric implements ModInitializer {
if (getConfig().veinMinerEnabled) { if (getConfig().veinMinerEnabled) {
PlayerBlockBreakEvents.BEFORE.register((world, player, pos, state, blockEntity) -> { PlayerBlockBreakEvents.BEFORE.register((world, player, pos, state, blockEntity) -> {
if (player instanceof ServerPlayerEntity serverPlayer) { if (player instanceof ServerPlayerEntity serverPlayer && getConfig().veinMinerEnabled) {
return VeinMinerEvents.beforeBlockBreak(world, serverPlayer, pos, state); return VeinMinerEvents.beforeBlockBreak(world, serverPlayer, pos, state);
} else { } else {
return true; return true;

View File

@ -8,7 +8,7 @@ import java.util.List;
public class Config { public class Config {
public String configVersion = "1.0"; public String configVersion = "1.1";
public boolean joinMessageEnabled = true; public boolean joinMessageEnabled = true;
public String joinMessage = "§8[§a+§8] §7%s"; public String joinMessage = "§8[§a+§8] §7%s";
@ -29,17 +29,20 @@ public class Config {
public boolean sharedEnderChestEnabled = true; public boolean sharedEnderChestEnabled = true;
public String sharedEnderChestName = "Ender Chest"; public String sharedEnderChestName = "Ender Chest";
public int sharedEnderChestRows = 6; public int sharedEnderChestRows = 6;
public boolean sharedEnderChestLimitedAccess = true; public boolean sharedEnderChestLimitedAccess = false;
public List<String> sharedEnderChestNames = Lists.newArrayList("AlwaysHAK", "LunaticFox"); public List<String> sharedEnderChestNames = Lists.newArrayList("AlwaysHAK");
public String inSlimeChunkMessage = "§aYou're currently in a slime chunk"; public String inSlimeChunkMessage = "§aYou're currently in a slime chunk";
public String notInSlimeChunkMessage = "§cYou're currently not in a slime chunk"; public String notInSlimeChunkMessage = "§cYou're currently not in a slime chunk";
public boolean veinMinerEnabled = true; public boolean veinMinerEnabled = true;
public int maxVeinSize = 99999; public int maxVeinSize = 99999;
public long veinAnimationTicks = 0;
public boolean chatCalcEnabled = true; public boolean chatCalcEnabled = true;
public boolean replenishEnabled = false;
public ScreenHandlerType<GenericContainerScreenHandler> screenHandlerType() { public ScreenHandlerType<GenericContainerScreenHandler> screenHandlerType() {
return switch (sharedEnderChestRows) { return switch (sharedEnderChestRows) {
case 1 -> ScreenHandlerType.GENERIC_9X1; case 1 -> ScreenHandlerType.GENERIC_9X1;

View File

@ -1,6 +1,8 @@
package wtf.hak.survivalfabric.features.veinminer.drills; package wtf.hak.survivalfabric.features.veinminer.drills;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.MutableText; import net.minecraft.text.MutableText;
import net.minecraft.text.PlainTextContent; import net.minecraft.text.PlainTextContent;
@ -8,27 +10,77 @@ import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import wtf.hak.survivalfabric.features.veinminer.Drill; import wtf.hak.survivalfabric.features.veinminer.Drill;
import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession; import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession;
import wtf.hak.survivalfabric.utils.Scheduler;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static wtf.hak.survivalfabric.SurvivalFabric.LOGGER; import static wtf.hak.survivalfabric.SurvivalFabric.LOGGER;
import static wtf.hak.survivalfabric.config.ConfigManager.getConfig;
public class DrillBase implements Drill { public class DrillBase implements Drill {
protected VeinMinerSession session; protected VeinMinerSession session;
protected TagKey<Block> tag;
public DrillBase(VeinMinerSession session) { public DrillBase(VeinMinerSession session, TagKey<Block> tag) {
this.session = session; this.session = session;
this.tag = tag;
} }
@Override @Override
public boolean canHandle(BlockState blockId) { public boolean canHandle(BlockState blockState) {
return false; return blockState.isIn(tag);
} }
@Override @Override
public boolean drill(BlockPos blockPos) { public boolean drill(BlockPos startPos) {
return false; handleBlock(session.world.getBlockState(startPos).getBlock(), new HashSet<>(), startPos, 0);
return true;
}
private void handleBlock(Block initialBlock, Set<BlockPos> history, BlockPos pos, int brokenBlocks) {
if (brokenBlocks < getConfig().maxVeinSize) {
history.add(pos);
if (tryBreakBlock(pos)) {
brokenBlocks++;
int finalBrokenBlocks = brokenBlocks;
// Put everything in a list to avoid scheduling a lot of tasks.
Set<BlockPos> toBreak = new HashSet<>();
collectAdjacentBlocks(pos, history, initialBlock, toBreak);
long delay = getConfig().veinAnimationTicks;
// Use final or effectively final variables for lambda
final int[] finalBrokenBlocksArr = {finalBrokenBlocks};
if (delay <= 0) {
handleBlocksInBatch(toBreak, initialBlock, history, finalBrokenBlocksArr[0]);
} else {
Scheduler.get().scheduleTask(() -> handleBlocksInBatch(toBreak, initialBlock, history, finalBrokenBlocksArr[0]), delay);
}
}
}
}
private void collectAdjacentBlocks(BlockPos pos, Set<BlockPos> history, Block initialBlock, Set<BlockPos> toBreak) {
forXYZ(pos, 1, new ForXYZHandler() {
@Override
public void handle(BlockPos newPos) {
if (!history.contains(newPos) && session.world.getBlockState(newPos).getBlock() == initialBlock) {
toBreak.add(newPos);
}
}
});
}
private void handleBlocksInBatch(Set<BlockPos> toBreak, Block initialBlock, Set<BlockPos> history, int finalBrokenBlocks) {
for (BlockPos newPos : toBreak) {
handleBlock(initialBlock, history, newPos, finalBrokenBlocks);
}
} }
@Override @Override
@ -38,9 +90,12 @@ public class DrillBase implements Drill {
} }
protected void forXYZ(BlockPos pos, int max, ForXYZHandler handler) { protected void forXYZ(BlockPos pos, int max, ForXYZHandler handler) {
forXYZ(pos, max, handlerPos -> { forXYZ(pos, max, new ForXYZCounter() {
handler.handle(handlerPos); @Override
return 0; public int handle(BlockPos pos) {
handler.handle(pos);
return 1; // Accumulate one count for each position processed
}
}, false); }, false);
} }
@ -49,14 +104,17 @@ public class DrillBase implements Drill {
} }
protected void forXYZ(BlockPos pos, int max, ForXYZHandler handler, boolean forceVertical) { protected void forXYZ(BlockPos pos, int max, ForXYZHandler handler, boolean forceVertical) {
forXYZ(pos, max, handlerPos -> { forXYZ(pos, max, new ForXYZCounter() {
handler.handle(handlerPos); @Override
return 0; public int handle(BlockPos pos) {
handler.handle(pos);
return 1; // Accumulate one count for each position processed
}
}, forceVertical); }, forceVertical);
} }
protected int forXYZ(BlockPos pos, int max, ForXYZCounter handler, boolean forceVertical) { protected int forXYZ(BlockPos pos, int max, ForXYZCounter handler, boolean forceVertical) {
ArrayList<Integer> offsets = new ArrayList<Integer>(); Set<Integer> offsets = new HashSet<>();
for (int d = 0; d <= max; ++d) { for (int d = 0; d <= max; ++d) {
offsets.add(d); offsets.add(d);
if (d != -d) { if (d != -d) {
@ -64,39 +122,21 @@ public class DrillBase implements Drill {
} }
} }
String[] order = new String[]{"x", "y", "z"}; String[] order = determineOrder(forceVertical);
if (forceVertical) {
order = new String[]{"y", "x", "z"};
} else {
ServerPlayerEntity player = session.player;
boolean majorPitchChange = player.getPitch() < -45.0 || player.getPitch() > 45.0;
boolean majorYawChange = (player.getYaw() > 45.0 && player.getYaw() < 135.0) || (player.getYaw() < -45.0 && player.getYaw() > -135.0);
if (majorPitchChange) {
if (majorYawChange) {
order = new String[]{"y", "z", "x"};
} else {
order = new String[]{"y", "x", "z"};
}
} else {
if (majorYawChange) {
order = new String[]{"z", "y", "x"};
}
}
}
int counter = 0; int counter = 0;
for (int i1 : offsets) { for (int i1 : offsets) {
for (int i2 : offsets) { for (int i2 : offsets) {
for (int i3 : offsets) { for (int i3 : offsets) {
int ix = order[0] == "x" ? i1 : order[1] == "x" ? i2 : i3; int ix = order[0].equals("x") ? i1 : order[1].equals("x") ? i2 : i3;
int iy = order[0] == "y" ? i1 : order[1] == "y" ? i2 : i3; int iy = order[0].equals("y") ? i1 : order[1].equals("y") ? i2 : i3;
int iz = order[0] == "z" ? i1 : order[1] == "z" ? i2 : i3; int iz = order[0].equals("z") ? i1 : order[1].equals("z") ? i2 : i3;
int px = pos.getX() + ix; int px = pos.getX() + ix;
int py = pos.getY() + iy; int py = pos.getY() + iy;
int pz = pos.getZ() + iz; int pz = pos.getZ() + iz;
counter += handler.handle(new BlockPos(px, py, pz)); counter += handler.handle(new BlockPos(px, py, pz)); // Works with ForXYZCounter, returning an int
} }
} }
} }
@ -104,6 +144,29 @@ public class DrillBase implements Drill {
return counter; return counter;
} }
protected String[] determineOrder(boolean forceVertical) {
if (forceVertical) {
return new String[]{"y", "x", "z"};
} else {
ServerPlayerEntity player = session.player;
boolean majorPitchChange = player.getPitch() < -45.0 || player.getPitch() > 45.0;
boolean majorYawChange = (player.getYaw() > 45.0 && player.getYaw() < 135.0) || (player.getYaw() < -45.0 && player.getYaw() > -135.0);
if (majorPitchChange) {
if (majorYawChange) {
return new String[]{"y", "z", "x"};
} else {
return new String[]{"y", "x", "z"};
}
} else {
if (majorYawChange) {
return new String[]{"z", "y", "x"};
}
}
}
return new String[]{"x", "y", "z"};
}
protected boolean tryBreakBlock(BlockPos blockPos) { protected boolean tryBreakBlock(BlockPos blockPos) {
session.addPosition(blockPos); session.addPosition(blockPos);
boolean success = isRightTool(blockPos) && session.player.interactionManager.tryBreakBlock(blockPos); boolean success = isRightTool(blockPos) && session.player.interactionManager.tryBreakBlock(blockPos);
@ -127,4 +190,4 @@ public class DrillBase implements Drill {
protected interface ForXYZCounter { protected interface ForXYZCounter {
int handle(BlockPos pos); int handle(BlockPos pos);
} }
} }

View File

@ -1,62 +1,22 @@
package wtf.hak.survivalfabric.features.veinminer.drills; package wtf.hak.survivalfabric.features.veinminer.drills;
import net.minecraft.block.Block; import net.minecraft.item.Items;
import net.minecraft.block.BlockState;
import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.TagKey; import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession; import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession;
import java.util.ArrayDeque;
import static wtf.hak.survivalfabric.config.ConfigManager.getConfig;
public class LeavesDrill extends DrillBase { public class LeavesDrill extends DrillBase {
public static final TagKey<Block> leavesTag = TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "leaves"));
public LeavesDrill(VeinMinerSession session) { public LeavesDrill(VeinMinerSession session) {
super(session); super(session, TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "leaves")));
} }
@Override @Override
public boolean canHandle(BlockState blockState) { public boolean isRightTool(BlockPos pos) {
return blockState.isIn(leavesTag); var blockState = session.world.getBlockState(pos);
} return session.player.getMainHandStack().isSuitableFor(blockState) || session.player.getMainHandStack().getItem() == Items.SHEARS;
@Override
public boolean drill(BlockPos startPos) {
ServerWorld world = session.world;
Block initialBlock = world.getBlockState(startPos).getBlock();
int brokenLeaves = 0;
ArrayDeque<BlockPos> pending = new ArrayDeque<BlockPos>();
pending.add(startPos);
while (!pending.isEmpty() && brokenLeaves < getConfig().maxVeinSize) {
BlockPos leavesPos = pending.remove();
Block leavesBlock = world.getBlockState(leavesPos).getBlock();
if (tryBreakBlock(leavesPos)) {
if (leavesBlock == initialBlock) {
brokenLeaves += 1;
}
if (leavesBlock == initialBlock) {
// look around current block
forXYZ(leavesPos, 1, newPos -> {
BlockState newBlockState = world.getBlockState(newPos);
Block newBlock = newBlockState.getBlock();
boolean isSameOreBlock = newBlock == leavesBlock;
if (!pending.contains(newPos) && isSameOreBlock) {
pending.add(newPos);
}
});
}
}
}
return true;
} }
} }

View File

@ -1,62 +1,14 @@
package wtf.hak.survivalfabric.features.veinminer.drills; package wtf.hak.survivalfabric.features.veinminer.drills;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.TagKey; import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession; import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession;
import java.util.ArrayDeque;
import static wtf.hak.survivalfabric.config.ConfigManager.getConfig;
public class OreDrill extends DrillBase { public class OreDrill extends DrillBase {
public static final TagKey<Block> oreTag = TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "ore"));
public OreDrill(VeinMinerSession session) { public OreDrill(VeinMinerSession session) {
super(session); super(session, TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "ore")));
}
@Override
public boolean canHandle(BlockState blockState) {
return blockState.isIn(oreTag);
}
@Override
public boolean drill(BlockPos startPos) {
ServerWorld world = session.world;
Block initialBlock = world.getBlockState(startPos).getBlock();
int brokenOre = 0;
ArrayDeque<BlockPos> pending = new ArrayDeque<BlockPos>();
pending.add(startPos);
while (!pending.isEmpty() && brokenOre < getConfig().maxVeinSize) {
BlockPos orePos = pending.remove();
Block oreBlock = world.getBlockState(orePos).getBlock();
if (tryBreakBlock(orePos)) {
if (oreBlock == initialBlock) {
brokenOre += 1;
}
if (oreBlock == initialBlock) {
// look around current block
forXYZ(orePos, 1, newPos -> {
BlockState newBlockState = world.getBlockState(newPos);
Block newBlock = newBlockState.getBlock();
boolean isSameOreBlock = newBlock == oreBlock;
if (!pending.contains(newPos) && isSameOreBlock) {
pending.add(newPos);
}
});
}
}
}
return true;
} }
} }

View File

@ -1,80 +1,14 @@
package wtf.hak.survivalfabric.features.veinminer.drills; package wtf.hak.survivalfabric.features.veinminer.drills;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.TagKey; import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession; import wtf.hak.survivalfabric.features.veinminer.VeinMinerSession;
import java.util.ArrayDeque;
import static wtf.hak.survivalfabric.config.ConfigManager.getConfig;
public class WoodDrill extends DrillBase { public class WoodDrill extends DrillBase {
public static final TagKey<Block> woodTag = TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "wood"));
public WoodDrill(VeinMinerSession session) { public WoodDrill(VeinMinerSession session) {
super(session); super(session, TagKey.of(RegistryKeys.BLOCK, Identifier.of("survivalfabric", "wood")));
}
@Override
public boolean canHandle(BlockState blockState) {
return blockState.isIn(woodTag);
}
@Override
public boolean drill(BlockPos startPos) {
ServerWorld world = session.world;
int broken = 0;
ArrayDeque<BlockPos> pendingLogs = new ArrayDeque<>();
ArrayDeque<BlockPos> logBlocks = new ArrayDeque<>();
pendingLogs.add(startPos);
String leavesBlockId = Registries.BLOCK.getId(world.getBlockState(startPos).getBlock()).toString().replace("_log", "_leaves");
while (!pendingLogs.isEmpty() && broken < getConfig().maxVeinSize) {
BlockPos woodPos = pendingLogs.remove();
Block woodBlock = world.getBlockState(woodPos).getBlock();
if (tryBreakBlock(woodPos)) {
logBlocks.add(woodPos);
broken += 1;
// look around current block
forXYZ(woodPos, 1, newPos -> {
Block newBlock = world.getBlockState(newPos).getBlock();
if (newBlock == woodBlock && !pendingLogs.contains(newPos)) {
pendingLogs.add(newPos);
}
}, true);
}
}
ArrayDeque<BlockPos> pendingLeaves = logBlocks;
while (!pendingLeaves.isEmpty() && broken < getConfig().maxVeinSize) {
broken += forXYZ(pendingLeaves.remove(), 1, newPos -> {
int brokenLeaves = 0;
Block newBlock = world.getBlockState(newPos).getBlock();
String newBlockId = Registries.BLOCK.getId(newBlock).toString();
if (newBlockId.equals(leavesBlockId)) {
if (tryBreakBlock(newPos)) {
brokenLeaves += 1;
}
}
return brokenLeaves;
}, true);
}
return true;
}
private boolean isLeaf(Block block) {
return Registries.BLOCK.getId(block).toString().endsWith("_leaves");
} }
} }

View File

@ -0,0 +1,64 @@
package wtf.hak.survivalfabric.mixin;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.CropBlock;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.HoeItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.stat.Stats;
import net.minecraft.util.Hand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.GameMode;
import net.minecraft.world.World;
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.CallbackInfoReturnable;
import wtf.hak.survivalfabric.config.ConfigManager;
import java.util.List;
@Mixin(Block.class)
public abstract class BlockMixin {
@Inject(method = "onBreak", at = @At("HEAD"), cancellable = true)
public void onBreak(World world, BlockPos pos, BlockState state, PlayerEntity player, CallbackInfoReturnable<BlockState> cir) {
if (world.isClient()) return;
if (state.getBlock() instanceof CropBlock && ConfigManager.getConfig().replenishEnabled) {
ItemStack mainHand = player.getStackInHand(Hand.MAIN_HAND);
if (mainHand.getItem() instanceof HoeItem) {
Item seedItem = state.getBlock().asItem();
Block seedBlock = state.getBlock();
List<ItemStack> drops = Block.getDroppedStacks(state, (ServerWorld) world, pos, null, player, mainHand);
if (removeIfAvailable(drops, seedItem)) {
if (player.getGameMode() != GameMode.CREATIVE) {
for (ItemStack drop : drops) {
Block.dropStack(world, pos, drop);
}
player.incrementStat(Stats.USED.getOrCreateStat(seedItem));
mainHand.damage(1, player, EquipmentSlot.MAINHAND);
}
world.getServer().executeSync(() -> world.setBlockState(pos, seedBlock.getDefaultState()));
cir.setReturnValue(Blocks.AIR.getDefaultState());
cir.cancel();
}
}
}
}
private boolean removeIfAvailable(List<ItemStack> drops, Item item) {
for (ItemStack drop : drops) {
if (drop.getItem() == item) {
drop.decrement(1);
return true;
}
}
return false;
}
}

View File

@ -15,12 +15,16 @@ import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import wtf.hak.survivalfabric.commands.SpectatorCommand; import wtf.hak.survivalfabric.commands.SpectatorCommand;
import wtf.hak.survivalfabric.config.ConfigManager; import wtf.hak.survivalfabric.config.ConfigManager;
import wtf.hak.survivalfabric.utils.MathUtils;
import java.util.Set; import java.util.Set;
@Mixin(PlayerManager.class) @Mixin(PlayerManager.class)
public abstract class PlayerManagerMixin { public abstract class PlayerManagerMixin {
/**
* Send join message to joined player
*/
@Inject(method = {"onPlayerConnect"}, at = {@At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")}) @Inject(method = {"onPlayerConnect"}, at = {@At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")})
public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) { public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) {
if (ConfigManager.getConfig().joinMessageEnabled && !player.getServer().isSingleplayer()) { if (ConfigManager.getConfig().joinMessageEnabled && !player.getServer().isSingleplayer()) {
@ -29,6 +33,9 @@ public abstract class PlayerManagerMixin {
} }
} }
/**
* Modify join message broadcasted to server
*/
@ModifyArg(method = {"onPlayerConnect"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")) @ModifyArg(method = {"onPlayerConnect"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V"))
private Text onPlayerConnect(Text text) { private Text onPlayerConnect(Text text) {
if (ConfigManager.getConfig().joinMessageEnabled) { if (ConfigManager.getConfig().joinMessageEnabled) {
@ -38,6 +45,9 @@ public abstract class PlayerManagerMixin {
return text; return text;
} }
/**
* Get player out of specator mode if necessary
*/
@Inject(method = {"remove"}, at = {@At("HEAD")}) @Inject(method = {"remove"}, at = {@At("HEAD")})
public void onPlayerLeave(ServerPlayerEntity player, CallbackInfo ci) { public void onPlayerLeave(ServerPlayerEntity player, CallbackInfo ci) {
if (SpectatorCommand.spectating.containsKey(player)) { if (SpectatorCommand.spectating.containsKey(player)) {
@ -47,6 +57,9 @@ public abstract class PlayerManagerMixin {
} }
} }
/**
* Modify chat messages
*/
@Inject(method = {"broadcast(Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/network/message/MessageType$Parameters;)V"}, at = {@At("HEAD")}, cancellable = true) @Inject(method = {"broadcast(Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/network/message/MessageType$Parameters;)V"}, at = {@At("HEAD")}, cancellable = true)
private void onBroadcast(SignedMessage message, ServerPlayerEntity sender, MessageType.Parameters parameters, CallbackInfo ci) { private void onBroadcast(SignedMessage message, ServerPlayerEntity sender, MessageType.Parameters parameters, CallbackInfo ci) {
if (sender != null) { if (sender != null) {
@ -56,10 +69,10 @@ public abstract class PlayerManagerMixin {
String processedMessage = rawMessage; String processedMessage = rawMessage;
if (isCalcEnabled) { if (isCalcEnabled && MathUtils.hasSupportedOperator(rawMessage)) {
String expression = rawMessage.endsWith("=") ? rawMessage.substring(0, rawMessage.length() - 1).trim() : rawMessage; String expression = rawMessage.endsWith("=") ? rawMessage.substring(0, rawMessage.length() - 1).trim() : rawMessage;
try { try {
String result = String.valueOf(evaluateExpression(expression)); String result = String.valueOf(MathUtils.evaluateExpression(expression));
StringBuilder sb = new StringBuilder(rawMessage).append("§6"); StringBuilder sb = new StringBuilder(rawMessage).append("§6");
if (rawMessage.contains(" ")) sb.append(" "); if (rawMessage.contains(" ")) sb.append(" ");
@ -83,47 +96,4 @@ public abstract class PlayerManagerMixin {
} }
} }
private double evaluateExpression(String expression) {
return evaluate(expression.replaceAll("\\s", ""), new int[]{0});
}
private double evaluate(String expr, int[] index) {
double value = parseTerm(expr, index);
while (index[0] < expr.length()) {
char op = expr.charAt(index[0]);
if (op != '+' && op != '-') break;
index[0]++;
double nextTerm = parseTerm(expr, index);
value = (op == '+') ? value + nextTerm : value - nextTerm;
}
return value;
}
private double parseTerm(String expr, int[] index) {
double value = parseFactor(expr, index);
while (index[0] < expr.length()) {
char op = expr.charAt(index[0]);
if (op != '*' && op != '/') break;
index[0]++;
double nextFactor = parseFactor(expr, index);
value = (op == '*') ? value * nextFactor : value / nextFactor;
}
return value;
}
private double parseFactor(String expr, int[] index) {
if (expr.charAt(index[0]) == '(') {
index[0]++;
double value = evaluate(expr, index);
index[0]++; // Skip closing ')'
return value;
}
int start = index[0];
while (index[0] < expr.length() && (Character.isDigit(expr.charAt(index[0])) || expr.charAt(index[0]) == '.')) {
index[0]++;
}
return Double.parseDouble(expr.substring(start, index[0]));
}
} }

View File

@ -8,8 +8,11 @@ import org.spongepowered.asm.mixin.injection.ModifyArg;
import wtf.hak.survivalfabric.config.ConfigManager; import wtf.hak.survivalfabric.config.ConfigManager;
@Mixin(ServerPlayNetworkHandler.class) @Mixin(ServerPlayNetworkHandler.class)
public abstract class ServerPlayNetworkHandlerMixin { public class ServerPlayNetworkHandlerMixin {
/**
* Modify quit message
*/
@ModifyArg(method = {"cleanUp"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V")) @ModifyArg(method = {"cleanUp"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Z)V"))
private Text quitMessage(Text text) { private Text quitMessage(Text text) {
if (ConfigManager.getConfig().quitMessageEnabled) { if (ConfigManager.getConfig().quitMessageEnabled) {

View File

@ -10,8 +10,11 @@ import wtf.hak.survivalfabric.commands.SpectatorCommand;
import wtf.hak.survivalfabric.config.ConfigManager; import wtf.hak.survivalfabric.config.ConfigManager;
@Mixin(ServerPlayerEntity.class) @Mixin(ServerPlayerEntity.class)
public abstract class ServerPlayerEntityMixin { public class ServerPlayerEntityMixin {
/**
* Change player list name if enabled
*/
@Inject(method = "getPlayerListName", at = @At("HEAD"), cancellable = true) @Inject(method = "getPlayerListName", at = @At("HEAD"), cancellable = true)
private void changePlayerListName(CallbackInfoReturnable<Text> cir) { private void changePlayerListName(CallbackInfoReturnable<Text> cir) {
if (ConfigManager.getConfig().dimensionIndicatorEnabled) { if (ConfigManager.getConfig().dimensionIndicatorEnabled) {

View File

@ -12,6 +12,9 @@ import wtf.hak.survivalfabric.utils.PacketUtils;
@Mixin(ServerWorld.class) @Mixin(ServerWorld.class)
public class ServerWorldMixin { public class ServerWorldMixin {
/**
* Update List Names if needed
*/
@Inject(method = "onDimensionChanged", at = {@At("HEAD")}) @Inject(method = "onDimensionChanged", at = {@At("HEAD")})
public void onDimensionChange(Entity entity, CallbackInfo ci) { public void onDimensionChange(Entity entity, CallbackInfo ci) {
if (entity instanceof ServerPlayerEntity) { if (entity instanceof ServerPlayerEntity) {

View File

@ -0,0 +1,64 @@
package wtf.hak.survivalfabric.utils;
public class MathUtils {
public static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
public static float clamp(float value, float min, float max) {
return Math.max(min, Math.min(max, value));
}
public static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
public static boolean hasSupportedOperator(String msg) {
return msg.contains("+") || msg.contains("-") || msg.contains("*") || msg.contains("/");
}
public static double evaluateExpression(String expression) {
return evaluate(expression.replaceAll("\\s", ""), new int[]{0});
}
public static double evaluate(String expr, int[] index) {
double value = parseTerm(expr, index);
while (index[0] < expr.length()) {
char op = expr.charAt(index[0]);
if (op != '+' && op != '-') break;
index[0]++;
double nextTerm = parseTerm(expr, index);
value = (op == '+') ? value + nextTerm : value - nextTerm;
}
return value;
}
public static double parseTerm(String expr, int[] index) {
double value = parseFactor(expr, index);
while (index[0] < expr.length()) {
char op = expr.charAt(index[0]);
if (op != '*' && op != '/') break;
index[0]++;
double nextFactor = parseFactor(expr, index);
value = (op == '*') ? value * nextFactor : value / nextFactor;
}
return value;
}
public static double parseFactor(String expr, int[] index) {
if (expr.charAt(index[0]) == '(') {
index[0]++;
double value = evaluate(expr, index);
index[0]++; // Skip closing ')'
return value;
}
int start = index[0];
while (index[0] < expr.length() && (Character.isDigit(expr.charAt(index[0])) || expr.charAt(index[0]) == '.')) {
index[0]++;
}
return Double.parseDouble(expr.substring(start, index[0]));
}
}

View File

@ -0,0 +1,45 @@
package wtf.hak.survivalfabric.utils;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Scheduler {
private static Scheduler INSTANCE;
private final Map<Runnable, Long> tasks = new ConcurrentHashMap<>();
public Scheduler() {
ServerTickEvents.END_SERVER_TICK.register((server) -> {
for(Runnable task : tasks.keySet()) {
long delay = tasks.get(task);
if(delay <= 0) {
task.run();
tasks.remove(task);
} else {
tasks.put(task, delay-1);
}
}
});
}
public void scheduleTask(Runnable task) {
scheduleTask(task, 0L);
}
public void scheduleTask(Runnable task, long delay) {
tasks.put(task, delay);
}
public static Scheduler get() {
return INSTANCE;
}
public static Scheduler initialize() {
INSTANCE = new Scheduler();
return INSTANCE;
}
}

View File

@ -17,5 +17,6 @@
"key.survivalfabric.angle14": "204.98 / -41.68", "key.survivalfabric.angle14": "204.98 / -41.68",
"key.survivalfabric.angle15": "244.97 / -41.71", "key.survivalfabric.angle15": "244.97 / -41.71",
"category.survivalfabric.survivalfabric": "Survival Fabric", "category.survivalfabric.survivalfabric": "Survival Fabric",
"key.survivalfabric.camera": "/camera" "key.survivalfabric.camera": "/camera",
"key.survivalfabric.zoom": "Zoom"
} }

View File

@ -3,6 +3,7 @@
"package": "wtf.hak.survivalfabric.mixin", "package": "wtf.hak.survivalfabric.mixin",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"mixins": [ "mixins": [
"BlockMixin",
"PlayerManagerMixin", "PlayerManagerMixin",
"ServerPlayerEntityMixin", "ServerPlayerEntityMixin",
"ServerPlayNetworkHandlerMixin", "ServerPlayNetworkHandlerMixin",