mirror of
https://github.com/GTNewHorizons/StructureLib.git
synced 2026-11-08 20:44:59 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3aefd9fd | ||
|
|
c4b091888b | ||
|
|
68fbdf8a81 | ||
|
|
f3bf5fc1ea | ||
|
|
9f9ab7acd9 | ||
|
|
0fe5a74863 | ||
|
|
d650c173bf |
No files matched your search
@@ -3,6 +3,7 @@
|
||||
dependencies {
|
||||
api("com.github.GTNewHorizons:GTNHLib:0.11.21:dev")
|
||||
implementation("com.github.GTNewHorizons:NotEnoughItems:2.8.110-GTNH:dev")
|
||||
|
||||
testImplementation(platform('org.junit:junit-bom:5.9.2'))
|
||||
testImplementation('org.junit.jupiter:junit-jupiter')
|
||||
testRuntimeOnly ('org.junit.platform:junit-platform-launcher')
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.gtnewhorizon.structurelib;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.ChatComponentTranslation;
|
||||
import net.minecraft.util.MovingObjectPosition;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor;
|
||||
import com.gtnewhorizon.structurelib.net.SetChannelDataMessage;
|
||||
|
||||
/**
|
||||
* Handler for middle-click channel picking. When the player holds a hologram projector on the cursor and middle-clicks
|
||||
* a registered indicator item, the corresponding channel is set on the projector. The same applies in world when the
|
||||
* projector is held in hand and a registered indicator block is middle-clicked. Shift+middle-click wipes all channels.
|
||||
* Was made side neutral to support server-side validation at some point if necessary.
|
||||
*/
|
||||
public final class ChannelPickHandler {
|
||||
|
||||
private ChannelPickHandler() {}
|
||||
|
||||
/**
|
||||
* Handle a middle-click event while holding a hologram projector on the cursor.
|
||||
*/
|
||||
public static boolean handleMiddleClick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack,
|
||||
final boolean isSneaking) {
|
||||
return handlePick(player, holoStack, targetStack, isSneaking, true);
|
||||
}
|
||||
|
||||
public static boolean handleWorldPick(World world, EntityPlayer player, ItemStack holoStack,
|
||||
@Nullable MovingObjectPosition hit, final boolean isSneaking) {
|
||||
ItemStack targetStack = null;
|
||||
|
||||
if (hit != null) {
|
||||
Block block = world.getBlock(hit.blockX, hit.blockY, hit.blockZ);
|
||||
if (!block.isAir(world, hit.blockX, hit.blockY, hit.blockZ)) {
|
||||
targetStack = block.getPickBlock(hit, world, hit.blockX, hit.blockY, hit.blockZ, player);
|
||||
}
|
||||
}
|
||||
|
||||
// A null target still goes through so that shift+middle-click wipes even when aiming at nothing
|
||||
return handlePick(player, holoStack, targetStack, isSneaking, false);
|
||||
}
|
||||
|
||||
private static boolean handlePick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack,
|
||||
final boolean isSneaking, final boolean onCursor) {
|
||||
// Clear channels on shift+middle-click. The "press again to confirm" gesture is gated client-side
|
||||
// (see ChannelWipeConfirmation) before this method is ever called.
|
||||
if (isSneaking) {
|
||||
ChannelDataAccessor.wipeChannelData(holoStack);
|
||||
sendChannelData(player, holoStack, onCursor);
|
||||
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.cleared"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targetStack == null) return false; // nothing hovered, cancel silently
|
||||
|
||||
Collection<Map.Entry<String, Integer>> channels = ChannelDescription.iterate(targetStack);
|
||||
if (channels.isEmpty()) {
|
||||
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.nomapping"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assuming there's only 1 channel per item for now (same behavior as drag and drop)
|
||||
Map.Entry<String, Integer> first = channels.iterator().next();
|
||||
ChannelDataAccessor.setChannelData(holoStack, first.getKey(), first.getValue());
|
||||
sendChannelData(player, holoStack, onCursor);
|
||||
player.addChatMessage(
|
||||
new ChatComponentTranslation("structurelib.pickchannel.success", first.getKey(), first.getValue()));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player is the one triggering this update, so should already be in-sync.
|
||||
*/
|
||||
private static void sendChannelData(EntityPlayer player, ItemStack holoStack, boolean onCursor) {
|
||||
if (!(player instanceof EntityPlayerMP)) {
|
||||
StructureLib.net.sendToServer(new SetChannelDataMessage(holoStack, onCursor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.gtnewhorizon.structurelib;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.util.ChatComponentTranslation;
|
||||
|
||||
/**
|
||||
* Client-side-only confirmation gate for "wipe all channels" shift+middle-click gesture.
|
||||
*/
|
||||
public final class ChannelWipeConfirmation {
|
||||
|
||||
private static final long CONFIRM_WINDOW_MS = 5000;
|
||||
private static long armedAtMs = -1;
|
||||
|
||||
private ChannelWipeConfirmation() {}
|
||||
|
||||
/**
|
||||
* Call this before invoking {@link ChannelPickHandler} whenever the wipe gesture (shift+middle-click) is detected.
|
||||
*
|
||||
* @return true if this is a confirming second press within the window and the wipe should proceed now; false if
|
||||
* this press only armed the confirmation.
|
||||
*/
|
||||
public static boolean confirm(EntityPlayer player) {
|
||||
long now = Minecraft.getSystemTime();
|
||||
if (now - armedAtMs > CONFIRM_WINDOW_MS) {
|
||||
armedAtMs = now;
|
||||
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.confirmwipe"));
|
||||
return false;
|
||||
}
|
||||
armedAtMs = -1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -14,7 +15,6 @@ import net.minecraft.block.Block;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.client.gui.GuiNewChat;
|
||||
import net.minecraft.client.particle.EffectRenderer;
|
||||
import net.minecraft.client.particle.EntityFX;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.culling.Frustrum;
|
||||
@@ -30,7 +30,6 @@ import net.minecraft.util.IChatComponent;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.client.event.MouseEvent;
|
||||
import net.minecraftforge.client.event.RenderWorldLastEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.common.config.ConfigElement;
|
||||
@@ -41,7 +40,6 @@ import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import com.gtnewhorizon.structurelib.entity.fx.WeightlessParticleFX;
|
||||
import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger;
|
||||
import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage;
|
||||
import com.gtnewhorizon.structurelib.net.SetChannelDataMessage;
|
||||
|
||||
@@ -55,26 +53,12 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
|
||||
import cpw.mods.fml.common.network.FMLNetworkEvent;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
|
||||
public class ClientProxy extends CommonProxy {
|
||||
|
||||
private static final short[] RGBA_NO_TINT = { 255, 255, 255, 255 };
|
||||
private static final short[] RGBA_RED_TINT = { 255, 128, 128, 0 };
|
||||
/**
|
||||
* Vanilla {@link EffectRenderer} keeps at most 4000 particles per layer and evicts the surplus with an
|
||||
* {@code ArrayList.remove(0)}, so spawning more than that in one projection costs a lot of copying for particles
|
||||
* that are thrown away before they are ever drawn.
|
||||
*/
|
||||
private static final int MAX_SPAWNED_PARTICLES_PER_GROUP = 4000;
|
||||
private static final Map<HintParticleInfo, HintGroup> allHints = new HashMap<>();
|
||||
/**
|
||||
* Total number of hints held across {@link #allGroups} and {@link #currentHints}, i.e. what
|
||||
* {@link ConfigurationHandler#getMaxRenderCount()} is compared against. Not the same as {@code allHints.size()},
|
||||
* which is keyed by position and therefore collapses hints that several holograms put at the same spot.
|
||||
*/
|
||||
private static int storedHintCount;
|
||||
/**
|
||||
* All batches of hints.
|
||||
*/
|
||||
@@ -84,36 +68,12 @@ public class ClientProxy extends CommonProxy {
|
||||
* Current batch of hints. Belongs to the same logical multiblock.
|
||||
*/
|
||||
private static HintGroup currentHints;
|
||||
/**
|
||||
* Player position and squared radius captured when {@link #currentHints} was opened, so the per-hint radius test is
|
||||
* plain arithmetic instead of a player lookup and a config lookup.
|
||||
*/
|
||||
private static double hintOriginX, hintOriginY, hintOriginZ;
|
||||
private static double hintRadiusSq;
|
||||
/**
|
||||
* Decorative particles spawned for {@link #currentHints} so far, capped at
|
||||
* {@link #MAX_SPAWNED_PARTICLES_PER_GROUP}.
|
||||
*/
|
||||
private static int spawnedParticles;
|
||||
/**
|
||||
* Collection of all hints. Sorted from farthest (index 0) to nearest (index size - 1)
|
||||
* <p>
|
||||
* 10k should be more than enough, if not a bit wasteful
|
||||
*/
|
||||
private static final List<HintParticleInfo> allHintsForRender = new ArrayList<>(10000);
|
||||
/**
|
||||
* Hints with {@link HintParticleInfo#renderThrough} set, collected during the main render pass and drawn afterwards
|
||||
* with the depth test off. Keeping them out of the main pass means the depth test is toggled once per frame rather
|
||||
* than once per renderThrough boundary in the distance ordering.
|
||||
*/
|
||||
private static final List<HintParticleInfo> deferredRenderThrough = new ArrayList<>();
|
||||
/**
|
||||
* Sorts farthest first, so transparent hints blend back to front and the nearest
|
||||
* {@link ConfigurationHandler#getMaxRenderCount()} hints are the tail of the list. Reads the distance cached by
|
||||
* {@link HintParticleInfo#cacheDistance} rather than recomputing it for every comparison.
|
||||
*/
|
||||
private static final Comparator<HintParticleInfo> FARTHEST_FIRST = (a, b) -> Double
|
||||
.compare(b.distSqToPlayer, a.distSqToPlayer);
|
||||
/**
|
||||
* If the diff to current player position is too great sort the allHints. Initial value is very far off below y=0
|
||||
* but not huge enough to make a NaN.
|
||||
@@ -125,6 +85,10 @@ public class ClientProxy extends CommonProxy {
|
||||
* if true rebuild the allHints list from all hintOwners (and sort it)
|
||||
*/
|
||||
private static boolean allHintsDirty = false;
|
||||
/**
|
||||
* counter keeping track of how many renderThrough hint particle we have. enable optimization if no renderThrough
|
||||
*/
|
||||
private static int renderThrough;
|
||||
/**
|
||||
* A throttle map for local player. Integrated server will use the throttle map in super class instead.
|
||||
*/
|
||||
@@ -133,25 +97,20 @@ public class ClientProxy extends CommonProxy {
|
||||
@Override
|
||||
public void hintParticleTinted(World w, int x, int y, int z, IIcon[] icons, short[] RGBa) {
|
||||
ensureHinting();
|
||||
if (!isWithinHintRadius(x, y, z)) return;
|
||||
if (storedHintCount >= ConfigurationHandler.INSTANCE.getMaxRenderCount()) return;
|
||||
HintParticleInfo info = new HintParticleInfo(w, x, y, z, icons, RGBa);
|
||||
HintGroup existing = allHints.get(info);
|
||||
// this very projection already covered this position
|
||||
if (existing == currentHints) return;
|
||||
|
||||
// check and remove colliding holograms
|
||||
if (existing != null && ConfigurationHandler.INSTANCE.isRemoveCollidingHologram()) {
|
||||
allGroups.remove(existing);
|
||||
removeGroup(existing);
|
||||
if (ConfigurationHandler.INSTANCE.isRemoveCollidingHologram()) {
|
||||
HintGroup dupe = allHints.get(info);
|
||||
if (dupe != null && dupe != currentHints) {
|
||||
allGroups.remove(dupe);
|
||||
removeGroup(dupe);
|
||||
}
|
||||
}
|
||||
allHints.put(info, currentHints);
|
||||
currentHints.getHints().add(info);
|
||||
storedHintCount++;
|
||||
allHintsDirty = true;
|
||||
|
||||
if (spawnedParticles >= MAX_SPAWNED_PARTICLES_PER_GROUP) return;
|
||||
spawnedParticles++;
|
||||
EntityFX particle = new WeightlessParticleFX(
|
||||
w,
|
||||
x + RANDOM.nextFloat() * 0.5F,
|
||||
@@ -227,13 +186,10 @@ public class ClientProxy extends CommonProxy {
|
||||
}
|
||||
|
||||
static void removeGroup(HintGroup group) {
|
||||
List<HintParticleInfo> hints = group.getHints();
|
||||
for (int i = 0, size = hints.size(); i < size; i++) {
|
||||
HintParticleInfo hint = hints.get(i);
|
||||
// a later hologram may have claimed this position, in which case the mapping is not ours to drop
|
||||
if (allHints.get(hint) == group) allHints.remove(hint);
|
||||
for (HintParticleInfo hintParticleInfo : group.getHints()) {
|
||||
allHints.remove(hintParticleInfo);
|
||||
if (hintParticleInfo.renderThrough) renderThrough--;
|
||||
}
|
||||
storedHintCount -= hints.size();
|
||||
allHintsDirty = true;
|
||||
}
|
||||
|
||||
@@ -267,49 +223,25 @@ public class ClientProxy extends CommonProxy {
|
||||
public void startHinting(World w) {
|
||||
if (!w.isRemote) return;
|
||||
if (currentHints != null) endHinting(w);
|
||||
beginGroup();
|
||||
currentHints = new HintGroup();
|
||||
}
|
||||
|
||||
private void ensureHinting() {
|
||||
if (currentHints == null) beginGroup();
|
||||
}
|
||||
|
||||
private static void beginGroup() {
|
||||
int maxCoexisting = ConfigurationHandler.INSTANCE.getMaxCoexistingHologram();
|
||||
// >= because we are about to add one more
|
||||
while (!allGroups.isEmpty() && allGroups.size() >= maxCoexisting) {
|
||||
removeGroup(allGroups.remove(0));
|
||||
}
|
||||
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
|
||||
if (player == null) {
|
||||
hintRadiusSq = Double.POSITIVE_INFINITY;
|
||||
} else {
|
||||
hintOriginX = player.posX;
|
||||
hintOriginY = player.posY;
|
||||
hintOriginZ = player.posZ;
|
||||
double radius = ConfigurationHandler.INSTANCE.getMaxRenderRadius();
|
||||
hintRadiusSq = radius * radius;
|
||||
}
|
||||
spawnedParticles = 0;
|
||||
currentHints = new HintGroup();
|
||||
if (currentHints == null) currentHints = new HintGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endHinting(World w) {
|
||||
if (!w.isRemote || currentHints == null) return;
|
||||
while (!allGroups.isEmpty() && allGroups.size() >= ConfigurationHandler.INSTANCE.getMaxCoexistingHologram()) {
|
||||
allGroups.remove(0);
|
||||
}
|
||||
if (!currentHints.getHints().isEmpty()) allGroups.add(currentHints);
|
||||
// we use the player existence time here as some worlds don't really advance the time ticker
|
||||
currentHints.setCreationTime(getCurrentPlayer().ticksExisted);
|
||||
currentHints = null;
|
||||
}
|
||||
|
||||
private static boolean isWithinHintRadius(int x, int y, int z) {
|
||||
double dx = x + 0.5 - hintOriginX;
|
||||
double dy = y + 0.5 - hintOriginY;
|
||||
double dz = z + 0.5 - hintOriginZ;
|
||||
return dx * dx + dy * dy + dz * dz <= hintRadiusSq;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOverworldTime() {
|
||||
// there is no overworld, better just hope current world time is ok...
|
||||
@@ -375,7 +307,7 @@ public class ClientProxy extends CommonProxy {
|
||||
|
||||
private static class HintGroup {
|
||||
|
||||
private final List<HintParticleInfo> hints = new ArrayList<>();
|
||||
private final List<HintParticleInfo> hints = new LinkedList<>();
|
||||
private int creationTime = -1;
|
||||
|
||||
public List<HintParticleInfo> getHints() {
|
||||
@@ -399,7 +331,6 @@ public class ClientProxy extends CommonProxy {
|
||||
private final IIcon[] icons;
|
||||
private short[] tint;
|
||||
private boolean renderThrough;
|
||||
private double distSqToPlayer;
|
||||
|
||||
private final long creationTime = System.currentTimeMillis(); // use tick time instead maybe
|
||||
|
||||
@@ -425,7 +356,10 @@ public class ClientProxy extends CommonProxy {
|
||||
}
|
||||
|
||||
public void setRenderThrough() {
|
||||
// no need to invalidate allHintsForRender: the render pass partitions on this flag per frame
|
||||
if (!this.renderThrough) {
|
||||
ClientProxy.renderThrough += 1;
|
||||
allHintsDirty = true;
|
||||
}
|
||||
this.renderThrough = true;
|
||||
}
|
||||
|
||||
@@ -441,14 +375,18 @@ public class ClientProxy extends CommonProxy {
|
||||
return frustrum.isBoxInFrustum(x + 0.25, y + 0.25, z + 0.25, x + 0.75, y + 0.75, z + 0.75);
|
||||
}
|
||||
|
||||
public void draw(Tessellator tes, int transparency, double eyeX, double eyeY, double eyeZ, int eyeXint,
|
||||
int eyeYint, int eyeZint) {
|
||||
public void draw(Tessellator tes, double eyeX, double eyeY, double eyeZ, int eyeXint, int eyeYint,
|
||||
int eyeZint) {
|
||||
double size = 0.5;
|
||||
|
||||
int brightness = w.blockExists(x, 0, z) ? w.getLightBrightnessForSkyBlocks(x, y, z, 0) : 0;
|
||||
tes.setBrightness(brightness);
|
||||
|
||||
tes.setColorRGBA((int) (tint[0] * .9F), (int) (tint[1] * .95F), (int) (tint[2] * 1F), transparency);
|
||||
tes.setColorRGBA(
|
||||
(int) (tint[0] * .9F),
|
||||
(int) (tint[1] * .95F),
|
||||
(int) (tint[2] * 1F),
|
||||
ConfigurationHandler.INSTANCE.getHintTransparency());
|
||||
|
||||
double X = (x - eyeXint) + 0.25;
|
||||
double Y = (y - eyeYint) + 0.25;
|
||||
@@ -544,18 +482,8 @@ public class ClientProxy extends CommonProxy {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
private void cacheDistance(double playerX, double playerY, double playerZ) {
|
||||
double dx = x + 0.5 - playerX;
|
||||
double dy = y + 0.5 - playerY;
|
||||
double dz = z + 0.5 - playerZ;
|
||||
distSqToPlayer = dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private boolean isWithinRadius(double eyeX, double eyeY, double eyeZ, double radiusSq) {
|
||||
double dx = x + 0.5 - eyeX;
|
||||
double dy = y + 0.5 - eyeY;
|
||||
double dz = z + 0.5 - eyeZ;
|
||||
return dx * dx + dy * dy + dz * dz <= radiusSq;
|
||||
public double getSquareDistanceTo(Vec3 point) {
|
||||
return point.squareDistanceTo(x + 0.5, y + 0.5, z + 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +492,12 @@ public class ClientProxy extends CommonProxy {
|
||||
static boolean connected = false;
|
||||
static boolean configSent = false;
|
||||
|
||||
private void resetPlayerLocation() {
|
||||
lastPlayerPos.xCoord = Minecraft.getMinecraft().thePlayer.posX;
|
||||
lastPlayerPos.yCoord = Minecraft.getMinecraft().thePlayer.posY;
|
||||
lastPlayerPos.zCoord = Minecraft.getMinecraft().thePlayer.posZ;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onPlayerLogIn(FMLNetworkEvent.ClientConnectedToServerEvent e) {
|
||||
connected = true;
|
||||
@@ -585,86 +519,47 @@ public class ClientProxy extends CommonProxy {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onClientTick(ClientTickEvent e) {
|
||||
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
|
||||
if (e.phase != Phase.END || player == null) return;
|
||||
boolean sortRequired = false;
|
||||
// The allGroups is implicitly sorted by creationTime
|
||||
// here we exploit this ordering to reduce iteration size
|
||||
int deadline = player.ticksExisted - ConfigurationHandler.INSTANCE.getHintLifespan();
|
||||
int i;
|
||||
for (i = 0; i < allGroups.size(); i++) {
|
||||
if (allGroups.get(i).getCreationTime() > deadline) {
|
||||
break;
|
||||
if (e.phase == Phase.END && Minecraft.getMinecraft().thePlayer != null) {
|
||||
Vec3 playerPos = Minecraft.getMinecraft().thePlayer.getPosition(1);
|
||||
boolean sortRequired = false;
|
||||
// The allGroups is implicitly sorted by creationTime
|
||||
// here we exploit this ordering to reduce iteration size
|
||||
int deadline = Minecraft.getMinecraft().thePlayer.ticksExisted
|
||||
- ConfigurationHandler.INSTANCE.getHintLifespan();
|
||||
int i;
|
||||
for (i = 0; i < allGroups.size(); i++) {
|
||||
if (allGroups.get(i).getCreationTime() > deadline) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i != 0) {
|
||||
sortRequired = true;
|
||||
// clear this block and go no further
|
||||
List<HintGroup> toRemove = allGroups.subList(0, i);
|
||||
toRemove.forEach(ClientProxy::removeGroup);
|
||||
toRemove.clear();
|
||||
}
|
||||
if (allHintsDirty) {
|
||||
allHintsForRender.clear();
|
||||
// we need greatly efficient sequential access and mildly efficient sorting
|
||||
// rebuild is sort of rare, and it's expected to lag anyway
|
||||
// so no min-heap here
|
||||
for (HintGroup c : allGroups) allHintsForRender.addAll(c.getHints());
|
||||
sortRequired = true;
|
||||
}
|
||||
if (sortRequired || playerPos.squareDistanceTo(lastPlayerPos) > 1e-2) {
|
||||
// only redo sort if player moved some distance
|
||||
// default is 0.1 block
|
||||
// if there was a full rebuild, go sort it as well
|
||||
allHintsForRender.sort(Comparator.comparingDouble(info -> -info.getSquareDistanceTo(playerPos)));
|
||||
resetPlayerLocation();
|
||||
}
|
||||
}
|
||||
if (i != 0) {
|
||||
sortRequired = true;
|
||||
// clear this block and go no further
|
||||
List<HintGroup> toRemove = allGroups.subList(0, i);
|
||||
toRemove.forEach(ClientProxy::removeGroup);
|
||||
toRemove.clear();
|
||||
}
|
||||
if (allHintsDirty) {
|
||||
allHintsForRender.clear();
|
||||
// we need greatly efficient sequential access and mildly efficient sorting
|
||||
// rebuild is sort of rare, and it's expected to lag anyway
|
||||
// so no min-heap here
|
||||
for (HintGroup c : allGroups) allHintsForRender.addAll(c.getHints());
|
||||
allHintsDirty = false;
|
||||
sortRequired = true;
|
||||
}
|
||||
final int size = allHintsForRender.size();
|
||||
if (size == 0) return;
|
||||
double px = player.posX, py = player.posY, pz = player.posZ;
|
||||
if (sortRequired || lastPlayerPos.squareDistanceTo(px, py, pz) > resortThreshold(size)) {
|
||||
// only redo sort if player moved some distance
|
||||
// if there was a full rebuild, go sort it as well
|
||||
for (int j = 0; j < size; j++) allHintsForRender.get(j).cacheDistance(px, py, pz);
|
||||
allHintsForRender.sort(FARTHEST_FIRST);
|
||||
lastPlayerPos.xCoord = px;
|
||||
lastPlayerPos.yCoord = py;
|
||||
lastPlayerPos.zCoord = pz;
|
||||
}
|
||||
}
|
||||
|
||||
private static double resortThreshold(int size) {
|
||||
if (size > 100_000) return 64; // 8 blocks
|
||||
if (size > 10_000) return 4; // 2 blocks
|
||||
return 1e-2; // 0.1 blocks
|
||||
}
|
||||
}
|
||||
|
||||
public static class ForgeEventHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onMouseEvent(MouseEvent event) {
|
||||
final EntityPlayer player = Minecraft.getMinecraft().thePlayer;
|
||||
|
||||
if (player == null || player.isDead) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ItemStack heldItem = player.getHeldItem();
|
||||
|
||||
if (heldItem == null || !(heldItem.getItem() instanceof ItemConstructableTrigger)) return;
|
||||
|
||||
if (event.button != 2 || !event.buttonstate) return;
|
||||
|
||||
// Sink the middle click as soon as a projector is held, so vanilla pick block does not run
|
||||
event.setCanceled(true);
|
||||
|
||||
final boolean isSneaking = player.isSneaking();
|
||||
if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return;
|
||||
|
||||
ChannelPickHandler.handleWorldPick(
|
||||
player.worldObj,
|
||||
player,
|
||||
heldItem,
|
||||
Minecraft.getMinecraft().objectMouseOver,
|
||||
isSneaking);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onEntityJoinWorld(EntityJoinWorldEvent e) {
|
||||
if (e.world.isRemote && e.entity instanceof EntityClientPlayerMP) {
|
||||
@@ -677,29 +572,17 @@ public class ClientProxy extends CommonProxy {
|
||||
if (e.world.isRemote) {
|
||||
// flush hints. we are in a different world now.
|
||||
allHintsForRender.clear();
|
||||
deferredRenderThrough.clear();
|
||||
allHints.clear();
|
||||
allGroups.clear();
|
||||
currentHints = null;
|
||||
storedHintCount = 0;
|
||||
allHintsDirty = false;
|
||||
lastPlayerPos.yCoord = -1e30;
|
||||
renderThrough = 0;
|
||||
// clear throttles. hopefully a world switch is enough long as a cool down.
|
||||
localThrottleMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static int flushIfFull(Tessellator tes, int cubeDrawn) {
|
||||
if (cubeDrawn + 1 < 4096 / (6 * 2 * 3)) return cubeDrawn;
|
||||
tes.draw();
|
||||
tes.startDrawing(GL11.GL_TRIANGLES);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onRenderWorldLast(RenderWorldLastEvent e) {
|
||||
final int size = allHintsForRender.size();
|
||||
if (size == 0) return;
|
||||
if (allHintsForRender.isEmpty()) return;
|
||||
|
||||
// seriously, I'm not a OpenGL expert, so I'm probably doing a lot of very stupid stuff here.
|
||||
// Please consider contributing patches if you find something to optimize.
|
||||
@@ -719,15 +602,6 @@ public class ClientProxy extends CommonProxy {
|
||||
int i0 = (int) d0, i1 = (int) d1, i2 = (int) d2;
|
||||
frustrum.setPosition(d0, d1, d2);
|
||||
|
||||
// hoist every config lookup out of the hot loop
|
||||
final int transparency = ConfigurationHandler.INSTANCE.getHintTransparency();
|
||||
final double radius = ConfigurationHandler.INSTANCE.getMaxRenderRadius();
|
||||
final double radiusSq = radius * radius;
|
||||
// allHintsForRender is sorted farthest first, so the nearest hints the budget should keep are the tail of
|
||||
// the list. Skipping a prefix is O(1) and, unlike counting as we draw, does not make hints pop in and out
|
||||
// as the player turns around.
|
||||
final int from = Math.max(0, size - ConfigurationHandler.INSTANCE.getMaxRenderCount());
|
||||
|
||||
GL11.glPushMatrix();
|
||||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT); // TODO figure out original states
|
||||
// we need the back facing rendered because the thing is transparent
|
||||
@@ -736,41 +610,39 @@ public class ClientProxy extends CommonProxy {
|
||||
GL11.glEnable(GL11.GL_BLEND); // enable blend so it is transparent
|
||||
GL11.glBlendFunc(GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_SRC_ALPHA);
|
||||
// depth test begin as enabled
|
||||
boolean renderThrough = false;
|
||||
Minecraft.getMinecraft().renderEngine.bindTexture(TextureMap.locationBlocksTexture);
|
||||
GL11.glTranslated(-d0 + i0, -d1 + i1, -d2 + i2);
|
||||
Tessellator tes = Tessellator.instance;
|
||||
tes.startDrawing(GL11.GL_TRIANGLES);
|
||||
int cubeDrawn = 0;
|
||||
for (int i = from; i < size; i++) {
|
||||
for (int i = 0, allHintsForRenderSize = allHintsForRender.size(), cubeDrawn = 0; i
|
||||
< allHintsForRenderSize; i++, cubeDrawn++) {
|
||||
HintParticleInfo hint = allHintsForRender.get(i);
|
||||
if (!hint.isWithinRadius(d0, d1, d2, radiusSq)) continue;
|
||||
if (!hint.isInFrustrum(frustrum)) continue;
|
||||
if (hint.renderThrough) {
|
||||
// drawn in a second pass, so the depth test is toggled once rather than at every boundary
|
||||
deferredRenderThrough.add(hint);
|
||||
continue;
|
||||
if (renderThrough != hint.renderThrough) {
|
||||
if (i > 0) {
|
||||
p.endStartSection("Draw");
|
||||
tes.draw();
|
||||
tes.startDrawing(GL11.GL_TRIANGLES);
|
||||
cubeDrawn = 0;
|
||||
p.endStartSection("Prepare");
|
||||
}
|
||||
if (hint.renderThrough) GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
else GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
renderThrough = hint.renderThrough;
|
||||
}
|
||||
cubeDrawn = flushIfFull(tes, cubeDrawn);
|
||||
hint.draw(tes, transparency, d0, d1, d2, i0, i1, i2);
|
||||
cubeDrawn++;
|
||||
// Minecraft Tessellator draw in batch of 4096. This plays nicely with quads, but for trigs
|
||||
// it might occasionally end up with one half in first batch and the other half in later batch.
|
||||
// so we manually flush the buffer here if that would happen.
|
||||
// 6 faces per particle, 2 trig per face, 3 vertex per trig
|
||||
if ((cubeDrawn + 1) >= 4096 / (6 * 2 * 3)) {
|
||||
tes.draw();
|
||||
tes.startDrawing(GL11.GL_TRIANGLES);
|
||||
}
|
||||
hint.draw(tes, d0, d1, d2, i0, i1, i2);
|
||||
}
|
||||
p.endStartSection("Draw");
|
||||
tes.draw();
|
||||
|
||||
if (!deferredRenderThrough.isEmpty()) {
|
||||
p.endStartSection("Prepare");
|
||||
GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
tes.startDrawing(GL11.GL_TRIANGLES);
|
||||
cubeDrawn = 0;
|
||||
for (int i = 0, deferred = deferredRenderThrough.size(); i < deferred; i++) {
|
||||
cubeDrawn = flushIfFull(tes, cubeDrawn);
|
||||
deferredRenderThrough.get(i).draw(tes, transparency, d0, d1, d2, i0, i1, i2);
|
||||
cubeDrawn++;
|
||||
}
|
||||
p.endStartSection("Draw");
|
||||
tes.draw();
|
||||
deferredRenderThrough.clear();
|
||||
}
|
||||
p.endSection();
|
||||
|
||||
GL11.glPopAttrib();
|
||||
|
||||
@@ -17,7 +17,6 @@ import net.minecraftforge.common.MinecraftForge;
|
||||
import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage;
|
||||
import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage;
|
||||
|
||||
import cpw.mods.fml.common.event.FMLInitializationEvent;
|
||||
import cpw.mods.fml.common.event.FMLLoadCompleteEvent;
|
||||
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
@@ -65,8 +64,6 @@ public class CommonProxy {
|
||||
|
||||
public void preInit(FMLPreInitializationEvent e) {}
|
||||
|
||||
public void init(FMLInitializationEvent e) {}
|
||||
|
||||
public long getOverworldTime() {
|
||||
return MinecraftServer.getServer().getEntityWorld().getTotalWorldTime();
|
||||
}
|
||||
|
||||
@@ -28,10 +28,7 @@ public enum ConfigurationHandler {
|
||||
|
||||
private Configuration config;
|
||||
private int maxCoexistingHologram;
|
||||
private int maxRenderRadius;
|
||||
private int maxRenderCount;
|
||||
private boolean removeCollidingHologram;
|
||||
private boolean skipCorrectlyPlacedHints;
|
||||
private int hintLifespan;
|
||||
private int hintTransparency;
|
||||
private int autoPlaceBudget;
|
||||
@@ -67,31 +64,6 @@ public enum ConfigurationHandler {
|
||||
1,
|
||||
100,
|
||||
"An attempt will be made to prune old holograms when a new hologram is about to be projected");
|
||||
maxRenderRadius = config.getInt(
|
||||
"maxRenderRadius",
|
||||
"client.hologram",
|
||||
512,
|
||||
1,
|
||||
Integer.MAX_VALUE,
|
||||
"Maximum distance from the client player at which projected blocks are kept and rendered.");
|
||||
maxRenderCount = config.getInt(
|
||||
"maxRenderCount",
|
||||
"client.hologram",
|
||||
1_000_000,
|
||||
1,
|
||||
Integer.MAX_VALUE,
|
||||
"Maximum number of projected blocks kept for rendering across all holograms.\n"
|
||||
+ "Blocks that are already placed correctly are not projected and do not count towards this.\n"
|
||||
+ "When a hologram exceeds this, the blocks nearest to the player are the ones kept.");
|
||||
skipCorrectlyPlacedHints = config.getBoolean(
|
||||
"skipCorrectlyPlaced",
|
||||
"client.hologram",
|
||||
true,
|
||||
"Do not project a block that is already placed correctly, so the render budget is spent on the blocks\n"
|
||||
+ "that are actually still missing.\n"
|
||||
+ "This needs each structure element to be checked while the hologram is built. Elements are\n"
|
||||
+ "not strictly required to have a side effect free check, so turn this off if a multiblock\n"
|
||||
+ "misbehaves while being projected.");
|
||||
removeCollidingHologram = config.getBoolean(
|
||||
"removeColliding",
|
||||
"client.hologram",
|
||||
@@ -184,22 +156,10 @@ public enum ConfigurationHandler {
|
||||
return maxCoexistingHologram;
|
||||
}
|
||||
|
||||
public int getMaxRenderRadius() {
|
||||
return maxRenderRadius;
|
||||
}
|
||||
|
||||
public int getMaxRenderCount() {
|
||||
return maxRenderCount;
|
||||
}
|
||||
|
||||
public boolean isRemoveCollidingHologram() {
|
||||
return removeCollidingHologram;
|
||||
}
|
||||
|
||||
public boolean isSkipCorrectlyPlacedHints() {
|
||||
return skipCorrectlyPlacedHints;
|
||||
}
|
||||
|
||||
public int getHintLifespan() {
|
||||
return hintLifespan;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.gtnewhorizon.structurelib.util.XSTR;
|
||||
|
||||
import cpw.mods.fml.common.Mod;
|
||||
import cpw.mods.fml.common.SidedProxy;
|
||||
import cpw.mods.fml.common.event.FMLInitializationEvent;
|
||||
import cpw.mods.fml.common.event.FMLInterModComms;
|
||||
import cpw.mods.fml.common.event.FMLLoadCompleteEvent;
|
||||
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
|
||||
@@ -129,11 +128,6 @@ public class StructureLib {
|
||||
DevelopHelper.onPreInit();
|
||||
}
|
||||
|
||||
@Mod.EventHandler
|
||||
public void init(FMLInitializationEvent e) {
|
||||
proxy.init(e);
|
||||
}
|
||||
|
||||
@Mod.EventHandler
|
||||
public void loadComplete(FMLLoadCompleteEvent e) {
|
||||
proxy.loadComplete(e);
|
||||
|
||||
+4
-11
@@ -38,9 +38,7 @@ public class ChannelDataAccessor {
|
||||
* If given subchannel does not exist, masterStack may or may not be returned to save performance. Code within
|
||||
* StructureLib can depend on this behavior, you cannot.
|
||||
*
|
||||
* @param masterStack trigger item. can be the return value of another
|
||||
* {@link com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor#withChannel(net.minecraft.item.ItemStack, String)}
|
||||
* call
|
||||
* @param masterStack trigger item. can be the return value of another {@link #withChannel(ItemStack, String)} call
|
||||
* @param channel subchannel to use. will use current master channel data instead if this channel does not exist
|
||||
* Note: all channel identifiers are supposed to be lower case and not empty.
|
||||
* @return a trigger item with new master channel
|
||||
@@ -109,13 +107,13 @@ public class ChannelDataAccessor {
|
||||
*
|
||||
* @param masterStack trigger stack to check
|
||||
* @param channel channel identifier. Note: all channel identifiers are supposed to be lower case and not empty.
|
||||
* @param data positive subchannel data
|
||||
* @param data subchannel data. should always be a positive value
|
||||
*/
|
||||
public static void setChannelData(ItemStack masterStack, String channel, int data) {
|
||||
if (StringUtils.isEmpty(channel) || masterStack == null) throw new IllegalArgumentException();
|
||||
if (data < 1) throw new IllegalArgumentException("Channel data must be positive");
|
||||
if (StructureLibAPI.isDebugEnabled() && !channel.toLowerCase(Locale.ROOT).equals(channel))
|
||||
throw new IllegalArgumentException("Channel name can be lower case ONLY");
|
||||
if (data <= 0) throw new IllegalArgumentException();
|
||||
if (masterStack.stackTagCompound == null) masterStack.stackTagCompound = new NBTTagCompound();
|
||||
NBTTagCompound main = masterStack.stackTagCompound;
|
||||
if (!main.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND)) main.setTag(SECONDARY_HINT_TAG, new NBTTagCompound());
|
||||
@@ -148,12 +146,7 @@ public class ChannelDataAccessor {
|
||||
*/
|
||||
public static void wipeChannelData(ItemStack masterStack) {
|
||||
if (masterStack == null) throw new IllegalArgumentException();
|
||||
if (masterStack.stackTagCompound != null) {
|
||||
masterStack.stackTagCompound.removeTag(SECONDARY_HINT_TAG);
|
||||
if (masterStack.stackTagCompound.hasNoTags()) {
|
||||
masterStack.stackTagCompound = null;
|
||||
}
|
||||
}
|
||||
if (masterStack.stackTagCompound != null) masterStack.stackTagCompound.removeTag(SECONDARY_HINT_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,7 +26,7 @@ public class CommandConfigureChannels extends CommandBase {
|
||||
public String getCommandUsage(ICommandSender p_71518_1_) {
|
||||
return "/sl_channel <wipe/getall>\n" + "/sl_channel <get/unset> <channel name>\n"
|
||||
+ "/sl_channel <set> <channel name> <value>\n"
|
||||
+ "<channel name> can have space in it. Values below 1 clear the channel";
|
||||
+ "<channel name> can have space in it. <value> must be positive integer";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,14 +108,11 @@ public class CommandConfigureChannels extends CommandBase {
|
||||
throw new WrongUsageException("%s not a valid value", args[args.length - 1]);
|
||||
}
|
||||
|
||||
if (value < 1) {
|
||||
ChannelDataAccessor.unsetChannelData(heldItem, channel);
|
||||
} else {
|
||||
ChannelDataAccessor.setChannelData(heldItem, channel, value);
|
||||
}
|
||||
if (value <= 0) throw new WrongUsageException("%s not a valid value", args[args.length - 1]);
|
||||
|
||||
ChannelDataAccessor.setChannelData(heldItem, channel, value);
|
||||
player.inventoryContainer.detectAndSendChanges();
|
||||
sender.addChatMessage(
|
||||
new ChatComponentText(value < 1 ? channel + " no value" : channel + " value: " + value));
|
||||
sender.addChatMessage(new ChatComponentText(channel + " value: " + value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.gtnewhorizon.structurelib.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -8,9 +7,7 @@ import java.util.Set;
|
||||
|
||||
import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader;
|
||||
|
||||
import cpw.mods.fml.relauncher.FMLLaunchHandler;
|
||||
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
|
||||
@IFMLLoadingPlugin.MCVersion("1.7.10")
|
||||
public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
|
||||
@@ -47,11 +44,6 @@ public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
|
||||
|
||||
@Override
|
||||
public List<String> getMixins(Set<String> loadedCoreMods) {
|
||||
List<String> mixins = new ArrayList<>(
|
||||
Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"));
|
||||
if (FMLLaunchHandler.side() == Side.CLIENT) {
|
||||
mixins.add("MixinGuiContainer_PickChannel");
|
||||
}
|
||||
return mixins;
|
||||
return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld");
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package com.gtnewhorizon.structurelib.mixins.early;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.inventory.GuiContainer;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.inventory.Slot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import com.gtnewhorizon.structurelib.ChannelPickHandler;
|
||||
import com.gtnewhorizon.structurelib.ChannelWipeConfirmation;
|
||||
import com.gtnewhorizon.structurelib.StructureLib;
|
||||
import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger;
|
||||
|
||||
import codechicken.nei.NEIClientConfig;
|
||||
import codechicken.nei.guihook.GuiContainerManager;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
|
||||
@Mixin(value = GuiContainer.class, priority = 100)
|
||||
public abstract class MixinGuiContainer_PickChannel {
|
||||
|
||||
@Inject(method = "mouseClicked(III)V", at = @At("HEAD"), cancellable = true)
|
||||
private void slib$onMouseClicked(int mouseX, int mouseY, int mouseButton, CallbackInfo ci) {
|
||||
if (mouseButton != 2) return;
|
||||
|
||||
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
|
||||
if (player == null) return;
|
||||
|
||||
ItemStack cursorStack = player.inventory.getItemStack();
|
||||
if (cursorStack == null || !(cursorStack.getItem() instanceof ItemConstructableTrigger)) return;
|
||||
|
||||
// Sink middle click as soon as projector is picked up
|
||||
ci.cancel();
|
||||
|
||||
ItemStack hoveredStack = null;
|
||||
try {
|
||||
Slot slot = slib$invokeGetSlotAtPosition(mouseX, mouseY);
|
||||
if (slot != null) {
|
||||
hoveredStack = slot.getStack();
|
||||
}
|
||||
|
||||
// try NEI
|
||||
if (hoveredStack == null && Loader.isModLoaded("NotEnoughItems") && !NEIClientConfig.isHidden()) {
|
||||
hoveredStack = GuiContainerManager.getStackMouseOver((GuiContainer) (Object) this);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
StructureLib.LOGGER.error("Error while resolving hovered slot for channel pick", e);
|
||||
}
|
||||
|
||||
boolean isSneaking = GuiScreen.isShiftKeyDown();
|
||||
if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return;
|
||||
|
||||
ChannelPickHandler.handleMiddleClick(player, cursorStack, hoveredStack, isSneaking);
|
||||
}
|
||||
|
||||
@Invoker("getSlotAtPosition")
|
||||
protected abstract Slot slib$invokeGetSlotAtPosition(int x, int y);
|
||||
}
|
||||
@@ -23,22 +23,15 @@ import io.netty.buffer.ByteBuf;
|
||||
public class SetChannelDataMessage implements IMessage {
|
||||
|
||||
private final List<Map.Entry<String, Integer>> data = new ArrayList<>();
|
||||
private boolean onCursor;
|
||||
|
||||
public SetChannelDataMessage() {}
|
||||
|
||||
public SetChannelDataMessage(ItemStack trigger) {
|
||||
this(trigger, false);
|
||||
}
|
||||
|
||||
public SetChannelDataMessage(ItemStack trigger, final boolean onCursor) {
|
||||
this.onCursor = onCursor;
|
||||
ChannelDataAccessor.iterateChannelData(trigger).forEach(data::add);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBytes(ByteBuf buf) {
|
||||
onCursor = buf.readBoolean();
|
||||
data.clear();
|
||||
int size = ByteBufUtils.readVarShort(buf);
|
||||
for (int i = 0; i < size; i++) {
|
||||
@@ -48,7 +41,6 @@ public class SetChannelDataMessage implements IMessage {
|
||||
|
||||
@Override
|
||||
public void toBytes(ByteBuf buf) {
|
||||
buf.writeBoolean(onCursor);
|
||||
ByteBufUtils.writeVarShort(buf, data.size());
|
||||
for (Entry<String, Integer> e : data) {
|
||||
ByteBufUtils.writeUTF8String(buf, e.getKey());
|
||||
@@ -61,11 +53,11 @@ public class SetChannelDataMessage implements IMessage {
|
||||
@Override
|
||||
public IMessage onMessage(SetChannelDataMessage message, MessageContext ctx) {
|
||||
EntityPlayerMP player = ctx.getServerHandler().playerEntity;
|
||||
ItemStack target = message.onCursor ? player.inventory.getItemStack() : player.getHeldItem();
|
||||
if (target != null && target.getItem() instanceof ItemConstructableTrigger) {
|
||||
ChannelDataAccessor.wipeChannelData(target);
|
||||
ItemStack heldItem = player.getHeldItem();
|
||||
if (heldItem != null && heldItem.getItem() instanceof ItemConstructableTrigger) {
|
||||
ChannelDataAccessor.wipeChannelData(heldItem);
|
||||
for (Entry<String, Integer> e : message.data) {
|
||||
ChannelDataAccessor.setChannelData(target, e.getKey(), e.getValue());
|
||||
ChannelDataAccessor.setChannelData(heldItem, e.getKey(), e.getValue());
|
||||
}
|
||||
// since this is a set all channel request from the client, we would assume client already know
|
||||
// what this would look like on the client, so no sync
|
||||
@@ -73,7 +65,7 @@ public class SetChannelDataMessage implements IMessage {
|
||||
StructureLib.LOGGER.warn(
|
||||
"{} trying to set channel data on {}, which is not a hologram projector!",
|
||||
player.getUniqueID(),
|
||||
target);
|
||||
heldItem);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package com.gtnewhorizon.structurelib.structure;
|
||||
|
||||
/**
|
||||
* Describes a channel used by one structure definition.
|
||||
*
|
||||
* <p>
|
||||
* Channel values are integers stored on the constructable trigger item. Implementations may override the default
|
||||
* validation and normalization policy when a simple bounded range is not sufficient.
|
||||
* </p>
|
||||
*/
|
||||
public interface IChannelDefinition {
|
||||
|
||||
/**
|
||||
* Creates a channel with a bounded inclusive range and clamping normalization.
|
||||
*
|
||||
* @param channel channel identifier
|
||||
* @param minimumValue inclusive minimum value
|
||||
* @param maximumValue inclusive maximum value
|
||||
* @return bounded channel definition
|
||||
*/
|
||||
static IChannelDefinition bounded(String channel, int minimumValue, int maximumValue) {
|
||||
if (minimumValue < 1 || maximumValue < minimumValue) {
|
||||
throw new IllegalArgumentException("Invalid channel bounds");
|
||||
}
|
||||
return new IChannelDefinition() {
|
||||
|
||||
@Override
|
||||
public String getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinimumValue() {
|
||||
return minimumValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaximumValue() {
|
||||
return maximumValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel accepting every positive integer.
|
||||
*
|
||||
* @param channel channel identifier
|
||||
* @return unbounded channel definition
|
||||
*/
|
||||
static IChannelDefinition unbounded(String channel) {
|
||||
return () -> channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lower-case channel identifier.
|
||||
*
|
||||
* @return channel identifier
|
||||
*/
|
||||
String getChannel();
|
||||
|
||||
/**
|
||||
* Returns the smallest value advertised by this definition. The default is {@code 1}.
|
||||
*
|
||||
* @return minimum value
|
||||
*/
|
||||
default int getMinimumValue() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest value advertised by this definition. The default is {@link java.lang.Integer#MAX_VALUE}.
|
||||
*
|
||||
* @return maximum value
|
||||
*/
|
||||
default int getMaximumValue() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a value is already valid without normalization.
|
||||
*
|
||||
* @param value value to check
|
||||
* @return true when the value is valid
|
||||
*/
|
||||
default boolean isValueValid(int value) {
|
||||
return value >= getMinimumValue() && value <= getMaximumValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a stored value to the value used by the structure definition.
|
||||
*
|
||||
* @param value stored value
|
||||
* @return normalized value
|
||||
*/
|
||||
default int normalizeValue(int value) {
|
||||
return Math.max(getMinimumValue(), Math.min(getMaximumValue(), value));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package com.gtnewhorizon.structurelib.structure;
|
||||
import static com.gtnewhorizon.structurelib.structure.IStructureWalker.ignoreBlockUnloaded;
|
||||
import static com.gtnewhorizon.structurelib.structure.IStructureWalker.skipBlockUnloaded;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -12,7 +10,6 @@ import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import com.gtnewhorizon.structurelib.ConfigurationHandler;
|
||||
import com.gtnewhorizon.structurelib.StructureLib;
|
||||
import com.gtnewhorizon.structurelib.StructureLibAPI;
|
||||
import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor;
|
||||
@@ -59,48 +56,6 @@ import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
|
||||
*/
|
||||
public interface IStructureDefinition<T> {
|
||||
|
||||
/**
|
||||
* Returns channel definitions declared by this structure.
|
||||
*
|
||||
* @return an immutable or otherwise read-only channel collection
|
||||
*/
|
||||
default Collection<IChannelDefinition> getChannelDefinitions() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns channel definitions explicitly added by the structure builder.
|
||||
*
|
||||
* @return explicitly declared channel definitions
|
||||
*/
|
||||
default Collection<IChannelDefinition> getExplicitChannelDefinitions() {
|
||||
return getChannelDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a channel definition by identifier.
|
||||
*
|
||||
* @param channel channel identifier
|
||||
* @return channel definition, or {@code null} when it is not declared
|
||||
*/
|
||||
default IChannelDefinition getChannelDefinition(String channel) {
|
||||
for (IChannelDefinition definition : getChannelDefinitions()) {
|
||||
if (definition.getChannel().equals(channel)) return definition;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one channel using the standard trigger-item channel semantics.
|
||||
*
|
||||
* @param trigger trigger item containing channel data
|
||||
* @param channel channel identifier
|
||||
* @return stored channel data, or the trigger stack size when absent
|
||||
*/
|
||||
default int getChannelData(ItemStack trigger, String channel) {
|
||||
return ChannelDataAccessor.getChannelData(trigger, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used internally
|
||||
* <p>
|
||||
@@ -112,7 +67,7 @@ public interface IStructureDefinition<T> {
|
||||
* @throws java.util.NoSuchElementException if the given structure piece is not found and the moon phase perfectly
|
||||
* matches
|
||||
*/
|
||||
IStructureElement<T>[] getStructureFor(String name);
|
||||
IStructureElement<? super T>[] getStructureFor(String name);
|
||||
|
||||
boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC);
|
||||
|
||||
@@ -401,7 +356,7 @@ public interface IStructureDefinition<T> {
|
||||
* @return true if iteration completed successfully.
|
||||
* @param <T> type of context object.
|
||||
*/
|
||||
static <T> boolean iterate(T object, ItemStack trigger, IStructureElement<T>[] elements, World world,
|
||||
static <T> boolean iterate(T object, ItemStack trigger, IStructureElement<? super T>[] elements, World world,
|
||||
ExtendedFacing extendedFacing, int basePositionX, int basePositionY, int basePositionZ, int basePositionA,
|
||||
int basePositionB, int basePositionC, boolean hintsOnly, Boolean checkBlocksIfNotNullForceCheckAllIfTrue) {
|
||||
if (!world.isRemote && hintsOnly) {
|
||||
@@ -444,8 +399,6 @@ public interface IStructureDefinition<T> {
|
||||
return success;
|
||||
} else {
|
||||
if (hintsOnly) {
|
||||
// elements are not strictly required to have a side effect free check, hence the opt out
|
||||
final boolean skipCorrectlyPlaced = ConfigurationHandler.INSTANCE.isSkipCorrectlyPlacedHints();
|
||||
StructureUtility.iterateV2(
|
||||
elements,
|
||||
world,
|
||||
@@ -456,27 +409,14 @@ public interface IStructureDefinition<T> {
|
||||
basePositionA,
|
||||
basePositionB,
|
||||
basePositionC,
|
||||
new IStructureWalker<T>() {
|
||||
|
||||
@Override
|
||||
public boolean visit(IStructureElement<T> e, World w, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
if (skipCorrectlyPlaced && e.check(object, w, x, y, z)) return true;
|
||||
e.spawnHint(object, w, x, y, z, trigger);
|
||||
if (ChannelDataAccessor.hasSubChannel(trigger, StructureLibAPI.CHANNEL_SHOW_ERROR)
|
||||
&& !e.couldBeValid(object, w, x, y, z, trigger)) {
|
||||
StructureLibAPI.markHintParticleError(StructureLib.getCurrentPlayer(), w, x, y, z);
|
||||
}
|
||||
return true;
|
||||
ignoreBlockUnloaded((e, w, x, y, z, a, b, c) -> {
|
||||
e.spawnHint(object, world, x, y, z, trigger);
|
||||
if (ChannelDataAccessor.hasSubChannel(trigger, StructureLibAPI.CHANNEL_SHOW_ERROR)
|
||||
&& !e.couldBeValid(object, world, x, y, z, trigger)) {
|
||||
StructureLibAPI.markHintParticleError(StructureLib.getCurrentPlayer(), world, x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockNotLoaded(IStructureElement<T> e, World w, int x, int y, int z, int a,
|
||||
int b, int c) {
|
||||
e.spawnHint(object, w, x, y, z, trigger);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
return true;
|
||||
}),
|
||||
"spawnHint");
|
||||
} else {
|
||||
StructureUtility.iterateV2(
|
||||
|
||||
@@ -4,7 +4,6 @@ import static com.gtnewhorizon.structurelib.StructureLib.LOGGER;
|
||||
import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -29,20 +28,14 @@ import com.gtnewhorizon.structurelib.util.ItemStackPredicate.NBTMode;
|
||||
/**
|
||||
* Use StructureUtility to instantiate. These are the building blocks for your {@link IStructureDefinition}. It
|
||||
* represents what a particular element can be and how this particular element should be autoplaced.
|
||||
*
|
||||
* @param <T> Type of the context object. {@link IStructureElement} is contravariant over T, so parameters and fields of
|
||||
* type {@link IStructureElement} should be declared as {@code IStructureElement<? super T>}
|
||||
*/
|
||||
public interface IStructureElement<T> {
|
||||
|
||||
boolean check(T t, World world, int x, int y, int z);
|
||||
|
||||
/**
|
||||
* Returns the channels used by this element.
|
||||
*
|
||||
* @return lower-case channel identifiers
|
||||
*/
|
||||
default Collection<String> getChannels() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure (stateless and side effect free) function to test if current block could be valid. Used to give user hints
|
||||
* about incorrectly placed blocks given a controller and trigger item. If couldBeValid(...) == false for a set of
|
||||
@@ -223,7 +216,7 @@ public interface IStructureElement<T> {
|
||||
* Forget the messed up class dependency graph for now. this is just so convenient.
|
||||
*/
|
||||
default IStructureElementNoPlacement<T> noPlacement() {
|
||||
return new IStructureElementNoPlacement<T>() {
|
||||
return new IStructureElementNoPlacement<>() {
|
||||
|
||||
@Override
|
||||
public boolean check(T t, World world, int x, int y, int z) {
|
||||
|
||||
@@ -21,11 +21,11 @@ import com.google.common.collect.Iterables;
|
||||
*/
|
||||
public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
|
||||
IStructureElement<T>[] fallbacks();
|
||||
IStructureElement<? super T>[] fallbacks();
|
||||
|
||||
@Override
|
||||
default boolean check(T t, World world, int x, int y, int z) {
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
if (fallback.check(t, world, x, y, z)) {
|
||||
return true;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
|
||||
@Override
|
||||
default boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
if (fallback.couldBeValid(t, world, x, y, z, trigger)) {
|
||||
return true;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
|
||||
@Override
|
||||
default boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
if (fallback.spawnHint(t, world, x, y, z, trigger)) {
|
||||
return true;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
|
||||
@Override
|
||||
default boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
if (fallback.placeBlock(t, world, x, y, z, trigger)) {
|
||||
return true;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
@Override
|
||||
default List<String> getDescription(T context) {
|
||||
Set<String> descriptions = new LinkedHashSet<>();
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
List<String> desc = fallback.getDescription(context);
|
||||
if (desc != null) {
|
||||
descriptions.addAll(desc);
|
||||
@@ -82,7 +82,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
AutoPlaceEnvironment env) {
|
||||
Predicate<ItemStack> predicate = null;
|
||||
List<Iterable<ItemStack>> is = new ArrayList<>();
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
BlocksToPlace e = fallback.getBlocksToPlace(t, world, x, y, z, trigger, env);
|
||||
if (e == null) continue;
|
||||
if (predicate == null) predicate = e.getPredicate();
|
||||
@@ -99,7 +99,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
EntityPlayerMP actor, Consumer<IChatComponent> chatter) {
|
||||
boolean allContinue = true;
|
||||
List<IChatComponent> bufferedNoise = new ArrayList<>();
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
PlaceResult result = fallback.survivalPlaceBlock(t, world, x, y, z, trigger, s, actor, bufferedNoise::add);
|
||||
switch (result) {
|
||||
case REJECT_CONTINUE:
|
||||
@@ -124,7 +124,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
|
||||
AutoPlaceEnvironment env) {
|
||||
boolean allContinue = true;
|
||||
List<IChatComponent> bufferedNoise = new ArrayList<>();
|
||||
for (IStructureElement<T> fallback : fallbacks()) {
|
||||
for (IStructureElement<? super T> fallback : fallbacks()) {
|
||||
PlaceResult result = fallback
|
||||
.survivalPlaceBlock(t, world, x, y, z, trigger, env.withChatter(bufferedNoise::add));
|
||||
switch (result) {
|
||||
|
||||
@@ -10,36 +10,36 @@ import net.minecraft.world.World;
|
||||
/**
|
||||
* Use StructureUtility to instantiate
|
||||
*/
|
||||
interface IStructureNavigate<T> extends IStructureElement<T> {
|
||||
interface IStructureNavigate extends IStructureElement<Object> {
|
||||
|
||||
@Override
|
||||
default boolean check(T t, World world, int x, int y, int z) {
|
||||
default boolean check(Object t, World world, int x, int y, int z) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
default boolean couldBeValid(Object t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
default boolean spawnHint(Object t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
default boolean placeBlock(Object t, World world, int x, int y, int z, ItemStack trigger) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s,
|
||||
default PlaceResult survivalPlaceBlock(Object t, World world, int x, int y, int z, ItemStack trigger, IItemSource s,
|
||||
EntityPlayerMP actor, Consumer<IChatComponent> chatter) {
|
||||
return PlaceResult.SKIP;
|
||||
}
|
||||
|
||||
@Override
|
||||
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger,
|
||||
default PlaceResult survivalPlaceBlock(Object t, World world, int x, int y, int z, ItemStack trigger,
|
||||
AutoPlaceEnvironment env) {
|
||||
return PlaceResult.SKIP;
|
||||
}
|
||||
|
||||
@@ -21,40 +21,42 @@ public interface IStructureWalker<T> {
|
||||
* @param c The relative C position of the element in structure-local coordinates (pre transform)
|
||||
* @return False to stop iterating, true to continue.
|
||||
*/
|
||||
boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c);
|
||||
boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b, int c);
|
||||
|
||||
default boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
|
||||
default boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static <T> IStructureWalker<T> ignoreBlockUnloaded(IStructureWalker<T> walker) {
|
||||
return new IStructureWalker<T>() {
|
||||
return new IStructureWalker<>() {
|
||||
|
||||
@Override
|
||||
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
|
||||
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
return walker.visit(element, world, x, y, z, a, b, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
public boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a,
|
||||
int b, int c) {
|
||||
return walker.visit(element, world, x, y, z, a, b, c);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static <T> IStructureWalker<T> skipBlockUnloaded(IStructureWalker<T> walker) {
|
||||
return new IStructureWalker<T>() {
|
||||
return new IStructureWalker<>() {
|
||||
|
||||
@Override
|
||||
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
|
||||
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
return walker.visit(element, world, x, y, z, a, b, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
|
||||
int c) {
|
||||
public boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a,
|
||||
int b, int c) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,14 +13,14 @@ import net.minecraft.world.World;
|
||||
|
||||
class LazyStructureElement<T> implements IStructureElementDeferred<T> {
|
||||
|
||||
private Function<T, IStructureElement<T>> to;
|
||||
private IStructureElement<T> elem;
|
||||
private Function<? super T, ? extends IStructureElement<? super T>> to;
|
||||
private IStructureElement<? super T> elem;
|
||||
|
||||
public LazyStructureElement(Function<T, IStructureElement<T>> to) {
|
||||
public LazyStructureElement(Function<? super T, ? extends IStructureElement<? super T>> to) {
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
private IStructureElement<T> get(T t) {
|
||||
private IStructureElement<? super T> get(T t) {
|
||||
if (to != null) {
|
||||
elem = to.apply(t);
|
||||
to = null;
|
||||
|
||||
@@ -5,12 +5,8 @@ import static com.gtnewhorizon.structurelib.structure.StructureUtility.notAir;
|
||||
import static com.gtnewhorizon.structurelib.structure.StructureUtility.step;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
@@ -20,31 +16,21 @@ import com.gtnewhorizon.structurelib.util.Vec3Impl;
|
||||
|
||||
public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
|
||||
private final Map<Character, IStructureElement<T>> elements;
|
||||
private final Map<Character, IStructureElement<? super T>> elements;
|
||||
private final Map<String, String> shapes;
|
||||
private final Map<String, IStructureElement<T>[]> structures;
|
||||
private final Map<String, OccupiedSpace> occupiedSpaces;
|
||||
private final Map<String, IChannelDefinition> channelDefinitions;
|
||||
private final Set<String> explicitChannels;
|
||||
private final Collection<IChannelDefinition> channelDefinitionValues;
|
||||
private final Collection<IChannelDefinition> explicitChannelDefinitions;
|
||||
private final Map<String, IStructureElement<? super T>[]> structures;
|
||||
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
|
||||
|
||||
public static <B> Builder<B> builder() {
|
||||
return new Builder<>();
|
||||
}
|
||||
|
||||
private StructureDefinition(Map<Character, IStructureElement<T>> elements, Map<String, String> shapes,
|
||||
Map<String, IStructureElement<T>[]> structures, Map<String, OccupiedSpace> occupiedSpaces,
|
||||
Map<String, IChannelDefinition> channelDefinitions, Set<String> explicitChannels) {
|
||||
private StructureDefinition(Map<Character, IStructureElement<? super T>> elements, Map<String, String> shapes,
|
||||
Map<String, IStructureElement<? super T>[]> structures, Map<String, Set<Vec3Impl>> occupiedSpaces) {
|
||||
this.elements = elements;
|
||||
this.shapes = shapes;
|
||||
this.structures = structures;
|
||||
this.occupiedSpaces = occupiedSpaces;
|
||||
this.channelDefinitions = Collections.unmodifiableMap(new HashMap<>(channelDefinitions));
|
||||
this.explicitChannels = Collections.unmodifiableSet(new HashSet<>(explicitChannels));
|
||||
this.channelDefinitionValues = Collections.unmodifiableCollection(this.channelDefinitions.values());
|
||||
this.explicitChannelDefinitions = Collections.unmodifiableList(
|
||||
this.explicitChannels.stream().map(this.channelDefinitions::get).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public static class Builder<T> {
|
||||
@@ -54,22 +40,18 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
private static final char C = '\uC000';
|
||||
private char d = '\uD000';
|
||||
private final Map<Vec3Impl, Character> navigates;
|
||||
private final Map<Character, IStructureElement<T>> elements;
|
||||
private final Map<Character, IStructureElement<? super T>> elements;
|
||||
private final Map<String, String> shapes;
|
||||
private final Map<String, OccupiedSpace> occupiedSpaces;
|
||||
private final Map<String, IChannelDefinition> channels;
|
||||
private final Set<String> explicitChannels;
|
||||
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
|
||||
|
||||
private Builder() {
|
||||
navigates = new HashMap<>();
|
||||
elements = new HashMap<>();
|
||||
shapes = new HashMap<>();
|
||||
occupiedSpaces = new HashMap<>();
|
||||
channels = new HashMap<>();
|
||||
explicitChannels = new HashSet<>();
|
||||
}
|
||||
|
||||
public Map<Character, IStructureElement<T>> getElements() {
|
||||
public Map<Character, IStructureElement<? super T>> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
@@ -113,19 +95,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
}
|
||||
builder.setLength(builder.length() - 1);
|
||||
}
|
||||
int sizeA = 0;
|
||||
int sizeB = 0;
|
||||
long occupiedCount = 0;
|
||||
for (String[] strings : structurePiece) {
|
||||
sizeB = Math.max(sizeB, strings.length);
|
||||
for (String string : strings) {
|
||||
sizeA = Math.max(sizeA, string.length());
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
if (string.charAt(i) != ' ') occupiedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
OccupiedSpace occupiedSpace = new OccupiedSpace(sizeA, sizeB, structurePiece.length, occupiedCount);
|
||||
Set<Vec3Impl> occupiedSpace = new HashSet<>();
|
||||
// these track the global current location
|
||||
int aa = 0, bb = 0, cc = 0;
|
||||
// these track the vec3 towards next meaningful element
|
||||
@@ -139,7 +109,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
} else if (ch == '~') {
|
||||
builder.setCharAt(i, A);
|
||||
ch = A;
|
||||
occupiedSpace.add(aa, bb, cc);
|
||||
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
|
||||
}
|
||||
if (ch == A) {
|
||||
aa++;
|
||||
@@ -165,13 +135,13 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
addElement(navigate, step(vec3));
|
||||
}
|
||||
builder.setCharAt(i - 1, navigate);
|
||||
occupiedSpace.add(aa, bb, cc);
|
||||
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
|
||||
aa++;
|
||||
a = 0;
|
||||
b = 0;
|
||||
c = 0;
|
||||
} else {
|
||||
occupiedSpace.add(aa, bb, cc);
|
||||
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
|
||||
aa++;
|
||||
}
|
||||
}
|
||||
@@ -211,66 +181,27 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
* @deprecated use the unboxed version
|
||||
*/
|
||||
@Deprecated
|
||||
public Builder<T> addElement(Character name, IStructureElement<T> structurePiece) {
|
||||
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
|
||||
public Builder<T> addElement(Character name, IStructureElement<? super T> structurePiece) {
|
||||
elements.putIfAbsent(name, structurePiece);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> addElement(char name, IStructureElement<T> structurePiece) {
|
||||
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
|
||||
public Builder<T> addElement(char name, IStructureElement<? super T> structurePiece) {
|
||||
elements.putIfAbsent(name, structurePiece);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> addChannel(IChannelDefinition definition) {
|
||||
validateChannelDefinition(definition);
|
||||
channels.put(definition.getChannel(), definition);
|
||||
explicitChannels.add(definition.getChannel());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> addChannel(String channel, int minimumValue, int maximumValue) {
|
||||
return addChannel(IChannelDefinition.bounded(channel, minimumValue, maximumValue));
|
||||
}
|
||||
|
||||
private void registerElementChannel(IStructureElement<T> structurePiece) {
|
||||
for (String channel : structurePiece.getChannels()) {
|
||||
validateChannelName(channel);
|
||||
if (!explicitChannels.contains(channel)) {
|
||||
channels.putIfAbsent(channel, IChannelDefinition.unbounded(channel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateChannelDefinition(IChannelDefinition definition) {
|
||||
if (definition == null) throw new IllegalArgumentException("Channel definition cannot be null");
|
||||
validateChannelName(definition.getChannel());
|
||||
if (definition.getMinimumValue() < 1 || definition.getMaximumValue() < definition.getMinimumValue()) {
|
||||
throw new IllegalArgumentException("Invalid channel definition values");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateChannelName(String channel) {
|
||||
if (channel == null || channel.isEmpty() || !channel.toLowerCase(Locale.ROOT).equals(channel)) {
|
||||
throw new IllegalArgumentException("Channel name must be non-empty and lower case");
|
||||
}
|
||||
}
|
||||
|
||||
public IStructureDefinition<T> build() {
|
||||
for (IStructureElement<T> element : elements.values()) {
|
||||
registerElementChannel(element);
|
||||
}
|
||||
Map<String, IStructureElement<T>[]> structures = compileStructureMap();
|
||||
Map<String, IStructureElement<? super T>[]> structures = compileStructureMap();
|
||||
return new StructureDefinition<>(
|
||||
new HashMap<>(elements),
|
||||
new HashMap<>(shapes),
|
||||
structures,
|
||||
occupiedSpaces,
|
||||
channels,
|
||||
explicitChannels);
|
||||
occupiedSpaces);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, IStructureElement<T>[]> compileElementSetMap() {
|
||||
private Map<String, IStructureElement<? super T>[]> compileElementSetMap() {
|
||||
Set<Integer> missing = findMissing();
|
||||
if (missing.isEmpty()) {
|
||||
return shapes.entrySet().stream().collect(
|
||||
@@ -286,7 +217,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, IStructureElement<T>[]> compileStructureMap() {
|
||||
private Map<String, IStructureElement<? super T>[]> compileStructureMap() {
|
||||
Set<Integer> missing = findMissing();
|
||||
if (missing.isEmpty()) {
|
||||
return shapes.entrySet().stream().collect(
|
||||
@@ -307,7 +238,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Character, IStructureElement<T>> getElements() {
|
||||
public Map<Character, IStructureElement<? super T>> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
@@ -315,77 +246,21 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
public Map<String, IStructureElement<T>[]> getStructures() {
|
||||
public Map<String, IStructureElement<? super T>[]> getStructures() {
|
||||
return structures;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IChannelDefinition> getChannelDefinitions() {
|
||||
return channelDefinitionValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IChannelDefinition> getExplicitChannelDefinitions() {
|
||||
return explicitChannelDefinitions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IChannelDefinition getChannelDefinition(String channel) {
|
||||
return channelDefinitions.get(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IStructureElement<T>[] getStructureFor(String name) {
|
||||
IStructureElement<T>[] elements = structures.get(name);
|
||||
public IStructureElement<? super T>[] getStructureFor(String name) {
|
||||
IStructureElement<? super T>[] elements = structures.get(name);
|
||||
if (elements == null) throw new NoSuchElementException(name);
|
||||
return elements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC) {
|
||||
OccupiedSpace occupiedSpace = occupiedSpaces.get(name);
|
||||
Set<Vec3Impl> occupiedSpace = occupiedSpaces.get(name);
|
||||
if (occupiedSpace == null) throw new NoSuchElementException(name);
|
||||
return occupiedSpace.contains(offsetA, offsetB, offsetC);
|
||||
}
|
||||
|
||||
private static class OccupiedSpace {
|
||||
|
||||
private final int sizeA;
|
||||
private final int sizeB;
|
||||
private final int sizeC;
|
||||
private final BitSet occupied;
|
||||
private final Set<Vec3Impl> sparseOccupied;
|
||||
|
||||
private OccupiedSpace(int sizeA, int sizeB, int sizeC, long occupiedCount) {
|
||||
this.sizeA = sizeA;
|
||||
this.sizeB = sizeB;
|
||||
this.sizeC = sizeC;
|
||||
long volume = (long) sizeA * sizeB * sizeC;
|
||||
if (volume <= Integer.MAX_VALUE && volume <= Math.max(1024L, occupiedCount * 256L)) {
|
||||
occupied = new BitSet((int) volume);
|
||||
sparseOccupied = null;
|
||||
} else {
|
||||
occupied = null;
|
||||
sparseOccupied = new HashSet<>();
|
||||
}
|
||||
}
|
||||
|
||||
private void add(int a, int b, int c) {
|
||||
if (occupied != null) {
|
||||
occupied.set(index(a, b, c));
|
||||
} else {
|
||||
sparseOccupied.add(new Vec3Impl(a, b, c));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(int a, int b, int c) {
|
||||
if (a < 0 || b < 0 || c < 0 || a >= sizeA || b >= sizeB || c >= sizeC) return false;
|
||||
if (occupied != null) return occupied.get(index(a, b, c));
|
||||
return sparseOccupied.contains(new Vec3Impl(a, b, c));
|
||||
}
|
||||
|
||||
private int index(int a, int b, int c) {
|
||||
return (c * sizeB + b) * sizeA + a;
|
||||
}
|
||||
return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -37,7 +37,7 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
|
||||
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b, int c) {
|
||||
env.offsetABC[0] = a;
|
||||
env.offsetABC[1] = b;
|
||||
env.offsetABC[2] = c;
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.gtnewhorizon.structurelib.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Spliterator;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SortedRegistry<V> implements Iterable<V> {
|
||||
|
||||
private final NavigableMap<String, V> store = new TreeMap<>();
|
||||
/**
|
||||
* Store stuff in an array for faster iteration access than a red black tree
|
||||
*/
|
||||
private List<V> baked = Collections.emptyList();
|
||||
|
||||
public void register(String key, V val) {
|
||||
if (key == null || val == null) throw new NullPointerException();
|
||||
V old = store.putIfAbsent(key, val);
|
||||
if (old != null) {
|
||||
throw new IllegalArgumentException("Duplicate key: " + key);
|
||||
}
|
||||
// TODO allow a user to edit the order with a configuration file
|
||||
baked = new ArrayList<>(store.values());
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return store.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return store.isEmpty();
|
||||
}
|
||||
|
||||
public boolean containsKey(String key) {
|
||||
return store.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean containsValue(V value) {
|
||||
return store.containsValue(value);
|
||||
}
|
||||
|
||||
public V get(String key) {
|
||||
return store.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<V> iterator() {
|
||||
return baked.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super V> action) {
|
||||
baked.forEach(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator<V> spliterator() {
|
||||
return baked.spliterator();
|
||||
}
|
||||
}
|
||||
@@ -174,37 +174,6 @@ StructureLib API documentation.
|
||||
master channel.
|
||||
</p>
|
||||
|
||||
<h3>Metadata-only channels</h3>
|
||||
|
||||
<p>
|
||||
A channel does not need to represent a block or an element in a structure shape. Structure definitions can declare
|
||||
metadata-only channels for values such as a widget selection or a variable structure height. These declarations are
|
||||
independent of <code>addElement</code> and do not require a fake block or a shape character.
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
IStructureDefinition.<Controller>builder()
|
||||
.addChannel("height", 3, 100)
|
||||
.addChannel("length", 4, 50)
|
||||
.addChannel(IChannelDefinition.unbounded("widget"))
|
||||
.addElement('A', ...)
|
||||
.build();
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Channel values are still stored as integers in the trigger item's <code>channels</code> NBT compound. The
|
||||
generic channel GUI has no active structure definition, so it edits integers and does not impose one
|
||||
structure's range on every other structure using the same channel name. Structure-aware code reads values through
|
||||
<code>IStructureDefinition#getChannelData</code>, which uses the definition's default when the subchannel is absent
|
||||
and then applies that definition's local normalization policy.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Existing elements wrapped with <code>StructureUtility.withChannel</code> are discovered automatically. An explicit
|
||||
channel declaration supplies its local range and normalization policy for that structure. The same channel name may
|
||||
therefore use different ranges in different structure definitions.
|
||||
</p>
|
||||
|
||||
<h3>why is this called channel?</h3>
|
||||
|
||||
Just give me a better name and I'd do the renaming.
|
||||
|
||||
@@ -44,11 +44,7 @@ item.structurelib.constructableTrigger.gui.info=Many multiblocks support some de
|
||||
channels.structurelib.show_errors=When set, will highlight parts of the multiblock that is definitely wrong
|
||||
|
||||
structurelib.tooltip.channelvalue=§7Value §b%d§r§7 for subchannel §b%s
|
||||
structurelib.tooltip.indicator_dnd=Drop into GUI or middle-click to select this value for this channel
|
||||
structurelib.pickchannel.success=§aSet §b%s§r§a to §b%d§r
|
||||
structurelib.pickchannel.cleared=§aAll channels cleared§r
|
||||
structurelib.pickchannel.confirmwipe=§6Sneak+Middle-click again to clear all channels
|
||||
structurelib.pickchannel.nomapping=§cNo channel mapping found for this item
|
||||
structurelib.tooltip.indicator_dnd=Drop into GUI to select this value for this channel
|
||||
|
||||
structurelib.autoplace.built_stat=Built %s blocks
|
||||
structurelib.autoplace.complete=§aThis multiblock has completed!
|
||||
@@ -187,12 +183,6 @@ structurelib.config.client.hologram.maxCoexisting=Max Coexisting Holograms
|
||||
structurelib.config.client.hologram.maxCoexisting.tooltip=Number of holograms that can coexist. Usually all hints generated by one right click belongs to one hologram. An attempt will be made to prune old holograms when a new hologram is about to be projected and this limit would be exceeded.
|
||||
structurelib.config.client.hologram.removeColliding=Remove Colliding Holograms
|
||||
structurelib.config.client.hologram.removeColliding.tooltip=Whether an attempt will be made to remove any existing hologram that collides with a new hologram.
|
||||
structurelib.config.client.hologram.maxRenderRadius=Maximum Hologram Render Radius
|
||||
structurelib.config.client.hologram.maxRenderRadius.tooltip=Maximum distance from the player at which projected blocks are kept and rendered.
|
||||
structurelib.config.client.hologram.maxRenderCount=Maximum Hologram Render Count
|
||||
structurelib.config.client.hologram.maxRenderCount.tooltip=Maximum number of projected blocks kept for rendering across all holograms. Blocks that are already placed correctly are not projected and do not count towards this limit. When a hologram exceeds this limit, the blocks nearest to the player are the ones kept.
|
||||
structurelib.config.client.hologram.skipCorrectlyPlaced=Skip Correctly Placed Blocks
|
||||
structurelib.config.client.hologram.skipCorrectlyPlaced.tooltip=Do not project a block that is already placed correctly, so the render budget is spent on the blocks that are actually still missing. This requires each structure element to be checked while the hologram is built. Elements are not strictly required to have a side effect free check, so turn this off if a multiblock misbehaves while being projected.
|
||||
|
||||
structurelib.config.registries=Sorted Registries
|
||||
structurelib.config.registries.tooltip=Sorted Registry is how StructureLib internally keep track of certain features. Each individual elements may be reordered, disabled and enabled at user's command. Changes will be synced to server if needed.\nThis gui can also be accessed in game by shift right clicking air with Multiblock Structure Hologram Projector.
|
||||
|
||||
@@ -181,12 +181,6 @@ structurelib.config.client.hologram.maxCoexisting=最多可以同时存在的全
|
||||
structurelib.config.client.hologram.maxCoexisting.tooltip=这决定最多可以同时存在多少台机器的全息投影。一般一次使用全息投影仪右键生成的所有机器结构细节被视为一个全息投影。当同时存在多个全息投影,并超过此数量时,再次使用全息投影仪会移除旧有的全息投影。
|
||||
structurelib.config.client.hologram.removeColliding=移除冲突的全息投影
|
||||
structurelib.config.client.hologram.removeColliding.tooltip=当新的全息投影与已存在的全息投影冲突时,开启此选项时会移除已存在的全息投影。
|
||||
structurelib.config.client.hologram.maxRenderRadius=全息投影最大渲染半径
|
||||
structurelib.config.client.hologram.maxRenderRadius.tooltip=只保留并渲染玩家指定距离内的投影方块。
|
||||
structurelib.config.client.hologram.maxRenderCount=全息投影最大渲染数量
|
||||
structurelib.config.client.hologram.maxRenderCount.tooltip=所有全息投影合计保留并渲染的投影方块数量上限。已正确放置的方块不会被投影,也不计入此上限。当全息投影超过此上限时,优先保留离玩家最近的方块。
|
||||
structurelib.config.client.hologram.skipCorrectlyPlaced=跳过已正确放置的方块
|
||||
structurelib.config.client.hologram.skipCorrectlyPlaced.tooltip=不再投影已经正确放置的方块,从而把渲染额度全部用在真正缺失的方块上。这需要在生成全息投影时检查每一个结构元素。结构元素并不强制要求其检查无副作用,因此若某个多方块在投影时出现异常,可关闭此选项。
|
||||
|
||||
structurelib.config.registries=注册表
|
||||
structurelib.config.registries.tooltip=StructureLib 使用注册表来管理其部分功能。每个注册表中的每一个元素都可以被单独排序、禁用。需要时,注册表的内容将被同步至服务端。
|
||||
|
||||
Reference in new issue
Block a user