Compare commits

..
6 Commits
16 changed files with 222 additions and 75 deletions

No files matched your search

@@ -25,7 +25,7 @@
return strippedBlock;
} else {
- Optional<BlockState> scrapedBlock = WeatheringCopper.getPrevious(oldState);
+ var scrape = ctx == null ? null : oldState.getToolModifiedState(ctx, net.minecraftforge.common.ToolActions.AXE_STRIP, false);
+ var scrape = ctx == null ? null : oldState.getToolModifiedState(ctx, net.minecraftforge.common.ToolActions.AXE_SCRAPE, false);
+ Optional<BlockState> scrapedBlock = scrape != null ? Optional.of(scrape) : WeatheringCopper.getPrevious(oldState);
if (scrapedBlock.isPresent()) {
spawnSoundAndParticle(level, pos, player, oldState, SoundEvents.AXE_SCRAPE, 3005);
@@ -97,7 +97,7 @@
List<Component> lines = Lists.newArrayList();
lines.add(this.getStyledHoverName());
this.addDetailsToTooltip(context, display, player, tooltipFlag, lines::add);
+ net.minecraftforge.event.ForgeEventFactory.onItemTooltip(this, player, lines, tooltipFlag);
+ net.minecraftforge.event.ForgeEventFactory.onItemTooltip(this, player, lines, tooltipFlag, context, display);
return lines;
}
}
@@ -107,7 +107,11 @@ public class ClientCommandHandler {
new CommandSource() {
@Override
public void sendSystemMessage(Component message) {
mc.gui.hud.getChat().addClientSystemMessage(message);
var chat = mc.gui.hud.getChat();
if (mc.isSameThread())
chat.addClientSystemMessage(message);
else
mc.execute(() -> chat.addClientSystemMessage(message));
}
@Override
@@ -75,6 +75,7 @@ public class ForgeConfig {
}
public final ForgeConfigSpec.EnumValue<MigrationHelperMode> migrationHelperMode;
public final ForgeConfigSpec.LongValue channelListLimit;
Common(ForgeConfigSpec.Builder builder) {
builder.comment("General configuration settings")
@@ -85,6 +86,17 @@ public class ForgeConfig {
.translation("forge.configgui.migrationHelperMode")
.defineEnum("logLegacyTagWarnings", MigrationHelperMode.OFF);
channelListLimit = builder
.comment(
"Maximum size to use to store the channel list for a network connection.",
"The other end of the connection may send as many channels as it wishes.",
"Some malicious actors may use this to use up all your memory causing OutOfMemoryException",
"A sane default is 1KB (1024), but may need to be expanded for larger modpacks.",
"Forge will auto-adjust this if mods register their channels using our channel manager."
)
.translation("forge.configgui.channellistlimit")
.defineInRange("channelListLimit", 1024, 1, Long.MAX_VALUE);
builder.pop();
}
}
@@ -15,6 +15,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@@ -52,6 +53,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.Connection;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.PacketListener;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.VarInt;
import net.minecraft.network.chat.ClickEvent;
@@ -104,6 +106,8 @@ import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.PacketFlow;
import net.minecraft.network.protocol.PacketType;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.network.protocol.common.custom.DiscardedPayload;
import net.minecraft.network.syncher.EntityDataSerializer;
@@ -1314,4 +1318,32 @@ public final class ForgeHooks {
return Optional.of(ret);
}
// This is similar to PacketUtils.ensureRunningOnSameThread, we can't use the normal LogicalSidedProvider.WORKQUEUE because it is processed after packets.
// So any vanilla packets that are received after this packet, will be processed before our enqueued packet.
public static <MSG> void enqueuePacket(BiConsumer<MSG, CustomPayloadEvent.Context> handler, MSG packet, CustomPayloadEvent.Context context) {
var processor = LogicalSidedProvider.PACKETS.get(context.isClientSide());
if (!processor.isSameThread())
processor.scheduleIfPossible(context.getConnection().getPacketListener(), new DummyPacket<>(handler, packet, context));
else
handler.accept(packet, context);
}
private static final record DummyPacket<MSG>(
BiConsumer<MSG, CustomPayloadEvent.Context> handler,
MSG packet,
CustomPayloadEvent.Context context
) implements Packet<PacketListener> {
private static final PacketType<DummyPacket<?>> TYPE = new PacketType<>(PacketFlow.CLIENTBOUND, Identifier.fromNamespaceAndPath("forge", "dummy_for_schedualing"));
@Override
public PacketType<DummyPacket<?>> type() {
return TYPE;
}
@Override
public void handle(PacketListener listener) {
handler.accept(packet, context);
}
}
}
@@ -6,6 +6,7 @@
package net.minecraftforge.common.util;
import net.minecraft.client.Minecraft;
import net.minecraft.network.PacketProcessor;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.TickTask;
import net.minecraft.util.thread.BlockableEventLoop;
@@ -21,6 +22,7 @@ import org.jetbrains.annotations.ApiStatus;
public class LogicalSidedProvider<T> {
public static final LogicalSidedProvider<BlockableEventLoop<? super TickTask>> WORKQUEUE = new LogicalSidedProvider<>(Supplier::get, Supplier::get);
public static final LogicalSidedProvider<Optional<Level>> CLIENTWORLD = new LogicalSidedProvider<>((c)-> Optional.of(c.get().level), (_)->Optional.empty());
public static final LogicalSidedProvider<PacketProcessor> PACKETS = new LogicalSidedProvider<>(client -> client.get().packetProcessor(), server -> server.get().packetProcessor());
public T get(LogicalSide side) {
return side == LogicalSide.CLIENT ? clientSide.apply(client) : serverSide.apply(server);
@@ -21,6 +21,7 @@ import net.minecraft.core.component.DataComponentMap;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.*;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraftforge.common.util.Result;
import org.jetbrains.annotations.ApiStatus;
@@ -354,8 +355,8 @@ public final class ForgeEventFactory {
return BlockEvent.FluidPlaceBlockEvent.BUS.fire(new BlockEvent.FluidPlaceBlockEvent(level, pos, liquidPos, state)).getNewState();
}
public static ItemTooltipEvent onItemTooltip(ItemStack itemStack, @Nullable Player entityPlayer, List<Component> list, TooltipFlag flags) {
return ItemTooltipEvent.BUS.fire(new ItemTooltipEvent(itemStack, entityPlayer, list, flags));
public static ItemTooltipEvent onItemTooltip(ItemStack itemStack, @Nullable Player entityPlayer, List<Component> list, TooltipFlag flags, Item.TooltipContext context, TooltipDisplay display) {
return ItemTooltipEvent.BUS.fire(new ItemTooltipEvent(itemStack, entityPlayer, list, flags, context, display));
}
public static SummonAidEvent fireZombieSummonAid(Zombie zombie, Level level, int x, int y, int z, LivingEntity attacker, double summonChance) {
@@ -8,12 +8,15 @@ package net.minecraftforge.event.entity.player;
import java.util.List;
import net.minecraft.client.Minecraft;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraftforge.eventbus.api.bus.EventBus;
import net.minecraftforge.eventbus.api.event.MutableEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,17 +28,40 @@ public final class ItemTooltipEvent extends MutableEvent implements PlayerEvent
@NotNull
private final ItemStack itemStack;
private final List<Component> toolTip;
private final Item.TooltipContext context;
private final TooltipDisplay display;
/**
* This event is fired in {@link ItemStack#getTooltipLines(Player, TooltipFlag)}, which in turn is called from its respective GUIContainer.
*
* This event is fired in {@link ItemStack#getTooltipLines(Item.TooltipContext, Player, TooltipFlag)}, which in turn is called from its respective GUIContainer.
* Tooltips are also gathered with a null player during startup by {@link Minecraft#createSearchTrees()}.
*/
public ItemTooltipEvent(@NotNull ItemStack itemStack, @Nullable Player player, List<Component> list, TooltipFlag flags)
@ApiStatus.Internal
public ItemTooltipEvent(@NotNull ItemStack itemStack, @Nullable Player player, List<Component> list, TooltipFlag flags, Item.TooltipContext context, TooltipDisplay display)
{
this.player = player;
this.itemStack = itemStack;
this.toolTip = list;
this.flags = flags;
this.context = context;
this.display = display;
}
/**
* The {@link net.minecraft.world.item.Item.TooltipContext} for this tooltip.
*/
public Item.TooltipContext getContext()
{
return context;
}
/**
* The {@link net.minecraft.world.item.component.TooltipDisplay} for this tooltip.
*/
public TooltipDisplay getDisplay()
{
return display;
}
/**
@@ -9,9 +9,11 @@ import com.mojang.logging.LogUtils;
import net.minecraft.IdentifierException;
import net.minecraft.network.Connection;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
import net.minecraftforge.common.ForgeConfig;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.event.network.ChannelRegistrationChangeEvent;
import net.minecraftforge.event.network.CustomPayloadEvent;
@@ -26,6 +28,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@ApiStatus.Internal // TODO: Decide what I want to have public, Right now it should just be add/remove channels.
public class ChannelListManager {
@@ -70,7 +73,7 @@ public class ChannelListManager {
}
private static void register(Register payload, CustomPayloadEvent.Context ctx) {
updateFrom(ctx, payload.channels(), ChannelRegistrationChangeEvent.Type.REGISTER);
updateFrom(ctx, payload.channels(), ChannelRegistrationChangeEvent.Type.REGISTER, ctx.isClientSide());
ctx.setPacketHandled(true);
// Send the client's channels to the server whenever asked. If we're in the login state then our login wrapper will unwrap for us.
if (ctx.isClientSide())
@@ -78,7 +81,7 @@ public class ChannelListManager {
}
private static void unregister(Unregister payload, CustomPayloadEvent.Context ctx) {
updateFrom(ctx, payload.channels(), ChannelRegistrationChangeEvent.Type.UNREGISTER);
updateFrom(ctx, payload.channels(), ChannelRegistrationChangeEvent.Type.UNREGISTER, ctx.isClientSide());
ctx.setPacketHandled(true);
}
@@ -145,7 +148,7 @@ public class ChannelListManager {
return Collections.unmodifiableList(channels);
}
private static void updateFrom(CustomPayloadEvent.Context source, List<String> channels, final ChannelRegistrationChangeEvent.Type changeType) {
private static void updateFrom(CustomPayloadEvent.Context source, List<String> channels, final ChannelRegistrationChangeEvent.Type changeType, boolean isClient) {
var changed = new HashSet<Identifier>();
for (var channel : channels) {
// It also says nothing about the format of channels so ignore bad channels.
@@ -162,11 +165,20 @@ public class ChannelListManager {
ForgeEventFactory.onChannelRegistrationChange(source.getConnection(), changeType, changed);
// It is a known issue, that I don't really care about, that we don't get the true in-memory size of each channel due to character encoding.
// A sane limit of 1KB will be enough to catch any malicious actors even with non-8-bit strings.
var list = NetworkContext.get(source.getConnection());
for (var channel : changed) {
boolean fire = changeType == ChannelRegistrationChangeEvent.Type.UNREGISTER
? list.remoteChannels.remove(channel)
: list.remoteChannels.add(channel);
boolean fire = false;
if (changeType == ChannelRegistrationChangeEvent.Type.UNREGISTER) {
fire = list.remoteChannels.remove(channel);
if (fire)
list.remoteChannelsSize -= channel.toString().length();
} else {
fire = list.remoteChannels.add(channel);
if (fire)
list.remoteChannelsSize += channel.toString().length();
}
if (fire) {
var target = NetworkRegistry.findTarget(channel);
@@ -174,5 +186,21 @@ public class ChannelListManager {
target.registrationChange(channel, changeType == ChannelRegistrationChangeEvent.Type.REGISTER);
}
}
if (list.remoteChannelsSize >= NetworkRegistry.getChannelMemorySize() && list.remoteChannelsSize >= ForgeConfig.COMMON.channelListLimit.get()) {
LOGGER.warn("Received too many network channels, {} channels using {} bytes, Limit is currently {}, to change edit the Forge {} in config\forge_common.toml",
list.remoteChannels.size(), list.remoteChannelsSize, ForgeConfig.COMMON.channelListLimit.get(), ForgeConfig.COMMON.channelListLimit.getPath().stream().collect(Collectors.joining("."))
);
if (isClient) {
// We don't want to allow the server to screw with us, so just remove the added channels
for (var channel : changed) {
if (list.remoteChannels.remove(channel))
list.remoteChannelsSize -= channel.toString().length();
}
} else {
source.getSender().connection.disconnect(Component.translatable("forge.server.channellistlimit.kick"));
}
}
}
}
@@ -111,6 +111,8 @@ public class NetworkContext {
@ApiStatus.Internal
Set<Identifier> remoteChannels = new HashSet<>();
@ApiStatus.Internal
long remoteChannelsSize = 0;
private Set<Identifier> remoteChannelsView = Collections.unmodifiableSet(this.remoteChannels);
@ApiStatus.Internal
@@ -43,6 +43,7 @@ public class NetworkRegistry {
private static Map<Identifier, NetworkInstance> instances = Collections.synchronizedMap(new HashMap<>());
private static Map<Identifier, NetworkInstance> byName = Collections.synchronizedMap(new HashMap<>());
private static long memorySize = 0;
public static boolean acceptsVanillaClientConnections() {
return listRejectedVanillaMods(n -> n.clientAcceptedVersions).isEmpty() && DataPackRegistriesHooks.getSyncedCustomRegistries().isEmpty();
@@ -199,9 +200,14 @@ public class NetworkRegistry {
if (NetworkRegistry.byName.containsKey(name))
error("Payload name " + name + " already registered.");
memorySize += name.toString().length(); // This can fail if people don't use our api. So this is a best effort.
byName.put(name, instance);
}
static long getChannelMemorySize() {
return memorySize;
}
static void register(NetworkInstance instance) {
checkLock(instance);
@@ -28,7 +28,7 @@ public interface SimpleFlow<BUF extends FriendlyByteBuf, BASE> extends SimplePro
*/
default <MSG extends BASE> SimpleFlow<BUF, BASE> addMain(Class<MSG> type, StreamCodec<BUF, MSG> codec, BiConsumer<MSG, CustomPayloadEvent.Context> handler) {
return add(type, codec, (msg, ctx) -> {
ctx.enqueueWork(() -> handler.accept(msg, ctx));
net.minecraftforge.common.ForgeHooks.enqueuePacket(handler, msg, ctx);
ctx.setPacketHandled(true);
});
}
@@ -350,7 +350,7 @@ public class GameData {
if (forgeRegistry != null)
forgeRegistry.freeze();
LOGGER.debug(REGISTRIES, "Applying holder lookups: {}", registryKey.identifier());
ObjectHolderRegistry.applyObjectHolders(registryKey.identifier()::equals);
ObjectHolderRegistry.applyObjectHolders(registryKey.identifier());
LOGGER.debug(REGISTRIES, "Holder lookups applied: {}", registryKey.identifier());
} catch (Throwable t) {
aggregate.addSuppressed(t);
@@ -5,10 +5,12 @@
package net.minecraftforge.registries;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import net.minecraft.resources.Identifier;
import org.apache.logging.log4j.LogManager;
@@ -16,32 +18,34 @@ import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;
/**
* Internal registry for tracking {@link ObjectHolder} references
* Internal registry for tracking {@link RegistryObject} references
*/
@ApiStatus.Internal
class ObjectHolderRegistry {
/**
* Exposed to allow modders to register their own notification handlers.
* This runnable will be called after a registry snapshot has been injected and finalized.
* The internal list is backed by a HashSet so it is HIGHLY recommended you implement a proper equals
* and hashCode function to de-duplicate callers here.
* The default @ObjectHolder implementation uses the hashCode/equals for the field the annotation is on.
* Registers a handler that is run after the named registry has been injected and finalized.
* Handlers are kept in a HashSet per registry, so the same instance is only ever registered once.
*/
static synchronized void addHandler(Consumer<Predicate<Identifier>> ref) {
objectHolders.add(ref);
static synchronized void addHandler(Identifier registry, Runnable ref) {
namedHandlers.computeIfAbsent(registry, _ -> new HashSet<>()).add(ref);
}
/**
* Removed the specified handler from the notification list.
*
* The internal list is backed by a hash set, and so proper hashCode and equals operations are required for success.
*
* The default @ObjectHolder implementation uses the hashCode/equals for the field the annotation is on.
* Removes a handler registered with {@link #addHandler(Identifier, Runnable)}.
*
* @return true if handler was matched and removed.
*/
static synchronized boolean removeHandler(Consumer<Predicate<Identifier>> ref) {
return objectHolders.remove(ref);
static synchronized boolean removeHandler(Identifier registry, Runnable ref) {
var handlers = namedHandlers.get(registry);
return handlers != null && handlers.remove(ref);
}
/**
* Registers a check that is run once, before the handlers of the next holder pass.
* Used to report holders that point at a registry that does not exist.
*/
static synchronized void addValidator(Runnable ref) {
validators.add(ref);
}
//==============================================================
@@ -49,12 +53,19 @@ class ObjectHolderRegistry {
//==============================================================
private static final Logger LOGGER = LogManager.getLogger();
private static final Set<Consumer<Predicate<Identifier>>> objectHolders = new HashSet<>();
// Order is not guaranteed, the old implementation kept every handler in one HashSet.
private static final Map<Identifier, Set<Runnable>> namedHandlers = new HashMap<>();
private static final List<Runnable> validators = new ArrayList<>();
/** Runs the handlers of every registry. */
static void applyObjectHolders() {
try {
LOGGER.debug(ForgeRegistry.REGISTRIES, "Applying holder lookups");
applyObjectHolders(_ -> true);
var exceptions = new ArrayList<Exception>();
validate(exceptions);
for (var handlers : namedHandlers.values())
apply(handlers, exceptions);
throwIfNeeded(exceptions);
LOGGER.debug(ForgeRegistry.REGISTRIES, "Holder lookups applied");
} catch (RuntimeException e) {
// It is more important that the calling contexts continue without exception to prevent further cascading errors
@@ -62,17 +73,52 @@ class ObjectHolderRegistry {
}
}
static void applyObjectHolders(Predicate<Identifier> filter) {
RuntimeException aggregate = new RuntimeException("Failed to apply some object holders, see suppressed exceptions for details");
for (Consumer<Predicate<Identifier>> objectHolder : objectHolders) {
try {
objectHolder.accept(filter);
} catch (Exception e) {
aggregate.addSuppressed(e);
}
/** Runs the handlers of one registry. */
static void applyObjectHolders(Identifier registry) {
var exceptions = new ArrayList<Exception>();
validate(exceptions);
var handlers = namedHandlers.get(registry);
if (handlers != null)
apply(handlers, exceptions);
throwIfNeeded(exceptions);
}
// Each validator runs once, at the first holder pass after it was registered.
private static void validate(List<Exception> exceptions) {
List<Runnable> pending;
synchronized (ObjectHolderRegistry.class) {
if (validators.isEmpty())
return;
pending = new ArrayList<>(validators);
validators.clear();
}
if (aggregate.getSuppressed().length > 0)
throw aggregate;
for (var validator : pending) {
try {
validator.run();
} catch (Exception e) {
exceptions.add(e);
}
}
}
private static void apply(Set<Runnable> handlers, List<Exception> exceptions) {
for (var handler : handlers) {
try {
handler.run();
} catch (Exception e) {
exceptions.add(e);
}
}
}
private static void throwIfNeeded(List<Exception> exceptions) {
if (exceptions.isEmpty())
return;
var aggregate = new RuntimeException("Failed to apply some object holders, see suppressed exceptions for details");
for (var e : exceptions)
aggregate.addSuppressed(e);
throw aggregate;
}
}
@@ -27,7 +27,6 @@ public final class RegistryObject<T> implements Supplier<T> {
private final Identifier name;
@Nullable
private final ResourceKey<T> key;
private final boolean optionalRegistry;
@Nullable
private T value;
@Nullable
@@ -129,7 +128,6 @@ public final class RegistryObject<T> implements Supplier<T> {
private RegistryObject() {
this.name = null;
this.key = null;
this.optionalRegistry = false;
}
@SuppressWarnings("unchecked")
@@ -138,40 +136,21 @@ public final class RegistryObject<T> implements Supplier<T> {
throw new IllegalArgumentException("Invalid registry argument, must not be null");
this.name = name;
this.key = (ResourceKey<T>) ResourceKey.create(registry.getRegistryKey(), name);
this.optionalRegistry = false;
ObjectHolderRegistry.addHandler(pred -> {
if (pred.test(registry.getRegistryName()))
this.updateReference((IForgeRegistry<? extends T>) registry);
});
ObjectHolderRegistry.addHandler(registry.getRegistryName(), () -> this.updateReference((IForgeRegistry<? extends T>) registry));
this.updateReference((IForgeRegistry<? extends T>) registry);
}
private RegistryObject(final Identifier name, final Identifier registryName, final String modid, boolean optionalRegistry) {
this.name = name;
this.key = ResourceKey.create(ResourceKey.createRegistryKey(registryName), name);
this.optionalRegistry = optionalRegistry;
final Throwable callerStack = new Throwable("Calling Site from mod: " + modid);
ObjectHolderRegistry.addHandler(new Consumer<>() {
private boolean registryExists = false;
private boolean invalidRegistry = false;
@Override
public void accept(Predicate<Identifier> pred) {
if (invalidRegistry)
return;
if (!RegistryObject.this.optionalRegistry && !registryExists) {
if (!registryExists(registryName)) {
invalidRegistry = true;
throw new IllegalStateException("Unable to find registry with key " + registryName + " for mod \"" + modid + "\". Check the 'caused by' to see further stack.", callerStack);
}
registryExists = true;
}
if (pred.test(registryName))
RegistryObject.this.updateReference(registryName);
}
});
if (!optionalRegistry) {
final Throwable callerStack = new Throwable("Calling Site from mod: " + modid);
ObjectHolderRegistry.addValidator(() -> {
if (!registryExists(registryName))
throw new IllegalStateException("Unable to find registry with key " + registryName + " for mod \"" + modid + "\". Check the 'caused by' to see further stack.", callerStack);
});
}
ObjectHolderRegistry.addHandler(registryName, () -> this.updateReference(registryName));
this.updateReference(registryName);
}
@@ -181,7 +160,6 @@ public final class RegistryObject<T> implements Supplier<T> {
@SuppressWarnings("unchecked")
var typed = (ResourceKey<Registry<T>>)registry;
this.key = ResourceKey.create(typed, name);
this.optionalRegistry = true;
}
/**
@@ -150,6 +150,10 @@
"forge.configgui.removeErroringBlockEntities": "Remove Erroring Block Entities",
"forge.configgui.fullBoundingBoxLadders.tooltip": "Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticeable differences in mechanics so default is vanilla behavior. Default: false.",
"forge.configgui.fullBoundingBoxLadders": "Full Bounding Box Ladders",
"forge.configgui.permissionHandler.tooltip": "The permission handler used by the server. Defaults to forge:default_handler if no such handler with that name is registered.",
"forge.configgui.permissionHandler": "Permission Handler",
"forge.configgui.advertiseDedicatedServerToLan.tooltip": "Set this to true to enable advertising the dedicated server to local LAN clients so that it shows up in the Multiplayer screen automatically.",
"forge.configgui.advertiseDedicatedServerToLan": "Advertise Dedicated Server to LAN",
"forge.configgui.zombieBaseSummonChance.tooltip": "Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic.",
"forge.configgui.zombieBaseSummonChance": "Zombie Summon Chance",
"forge.configgui.zombieBabyChance.tooltip": "Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic.",
@@ -200,6 +204,12 @@
"forge.configgui.stabilizeDirectionGetNearest": "Stabilize Direction Get Nearest",
"forge.configgui.stabilizeDirectionGetNearest.tooltip": "When enabled, a slightly biased Direction#getNearest calculation will be used to prevent normal fighting on 45 degree angle faces.",
"forge.configgui.channellistlimit": "Channel List Size Limit",
"forge.configgui.channellistlimit.tooltip": "Maximum size to use to store the channel list for a network connection. Some malicious actors may use this to use up all your memory. A sane default is 1KB (1024), but may need to be expanded for larger modpacks.",
"forge.configgui.migrationHelperMode": "Migration Helper Mode",
"forge.configgui.migrationHelperMode.tooltip": "A config option to help developers find known legacy modded tags that have common convention equivalents when running on integrated server. Defaults to OFF.",
"forge.server.channellistlimit.kick": "Received too many networking channels",
"forge.controlsgui.shift": "SHIFT + %s",
"forge.controlsgui.control": "CTRL + %s",
"forge.controlsgui.control.mac": "CMD + %s",