--- a/net/minecraft/world/level/GameRules.java
+++ b/net/minecraft/world/level/GameRules.java
@@ -35,9 +_,17 @@
 import org.slf4j.Logger;
 
 public class GameRules {
+    // Paper start - allow disabling gamerule limits
+    private static final boolean DISABLE_LIMITS = Boolean.getBoolean("paper.disableGameRuleLimits");
+
+    private static int limit(final int limit, final int unlimited) {
+        return DISABLE_LIMITS ? unlimited : limit;
+    }
+    // Paper end - allow disabling gamerule limits
+
     public static final int DEFAULT_RANDOM_TICK_SPEED = 3;
     static final Logger LOGGER = LogUtils.getLogger();
-    private static final Map<GameRules.Key<?>, GameRules.Type<?>> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing(entry -> entry.id));
+    public static final Map<GameRules.Key<?>, GameRules.Type<?>> GAME_RULE_TYPES = Maps.newTreeMap(Comparator.comparing(entry -> entry.id));
     public static final GameRules.Key<GameRules.BooleanValue> RULE_DOFIRETICK = register(
         "doFireTick", GameRules.Category.UPDATES, GameRules.BooleanValue.create(true)
     );
@@ -87,10 +_,10 @@
         "sendCommandFeedback", GameRules.Category.CHAT, GameRules.BooleanValue.create(true)
     );
     public static final GameRules.Key<GameRules.BooleanValue> RULE_REDUCEDDEBUGINFO = register(
-        "reducedDebugInfo", GameRules.Category.MISC, GameRules.BooleanValue.create(false, (server, value) -> {
+        "reducedDebugInfo", GameRules.Category.MISC, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
             byte b = (byte)(value.get() ? 22 : 23);
 
-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
+            for (ServerPlayer serverPlayer : level.players()) {
                 serverPlayer.connection.send(new ClientboundEntityEventPacket(serverPlayer, b));
             }
         })
@@ -114,8 +_,8 @@
         "doWeatherCycle", GameRules.Category.UPDATES, GameRules.BooleanValue.create(!SharedConstants.DEBUG_WORLD_RECREATE)
     );
     public static final GameRules.Key<GameRules.BooleanValue> RULE_LIMITED_CRAFTING = register(
-        "doLimitedCrafting", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (server, value) -> {
-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
+        "doLimitedCrafting", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
+            for (ServerPlayer serverPlayer : level.players()) {
                 serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LIMITED_CRAFTING, value.get() ? 1.0F : 0.0F));
             }
         })
@@ -139,8 +_,8 @@
         "doInsomnia", GameRules.Category.SPAWNING, GameRules.BooleanValue.create(true)
     );
     public static final GameRules.Key<GameRules.BooleanValue> RULE_DO_IMMEDIATE_RESPAWN = register(
-        "doImmediateRespawn", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (server, value) -> {
-            for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
+        "doImmediateRespawn", GameRules.Category.PLAYER, GameRules.BooleanValue.create(false, (level, value) -> { // Paper - rename param to match changes
+            for (ServerPlayer serverPlayer : level.players()) {
                 serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, value.get() ? 1.0F : 0.0F));
             }
         })
@@ -211,7 +_,7 @@
     public static final GameRules.Key<GameRules.IntegerValue> RULE_MINECART_MAX_SPEED = register(
         "minecartMaxSpeed",
         GameRules.Category.MISC,
-        GameRules.IntegerValue.create(8, 1, 1000, FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (server, value) -> {})
+        GameRules.IntegerValue.create(8, 1, limit(1000, Integer.MAX_VALUE), FeatureFlagSet.of(FeatureFlags.MINECART_IMPROVEMENTS), (server, value) -> {}) // Paper - allow disabling gamerule limits
     );
     public static final GameRules.Key<GameRules.BooleanValue> RULE_TNT_EXPLODES = register(
         "tntExplodes", GameRules.Category.MISC, GameRules.BooleanValue.create(true)
@@ -219,7 +_,7 @@
     public static final GameRules.Key<GameRules.BooleanValue> RULE_LOCATOR_BAR = register(
         "locatorBar",
         GameRules.Category.PLAYER,
-        GameRules.BooleanValue.create(true, (minecraftServer, booleanValue) -> minecraftServer.getAllLevels().forEach(serverLevel -> {
+        GameRules.BooleanValue.create(true, (minecraftServer, booleanValue) -> java.util.Optional.of(minecraftServer).ifPresent(serverLevel -> { // Paper - per world game rules
             ServerWaypointManager waypointManager = serverLevel.getWaypointManager();
             if (booleanValue.get()) {
                 serverLevel.players().forEach(waypointManager::updatePlayer);
@@ -235,7 +_,11 @@
     public static final GameRules.Key<GameRules.BooleanValue> RULE_SPAWN_MONSTERS = register(
         "spawnMonsters",
         GameRules.Category.SPAWNING,
-        GameRules.BooleanValue.create(true, (minecraftServer, booleanValue) -> minecraftServer.updateMobSpawningFlags())
+        // Paper start - per world gamerules
+        GameRules.BooleanValue.create(true, (level, booleanValue) -> {
+            level.setSpawnSettings(level.serverLevelData.getDifficulty() != net.minecraft.world.Difficulty.PEACEFUL && level.getGameRules().getBoolean(GameRules.RULE_SPAWN_MONSTERS));
+        })
+        // Paper end - per world gamerules
     );
     public static final GameRules.Key<GameRules.BooleanValue> RULE_COMMAND_BLOCKS_ENABLED = register(
         "commandBlocksEnabled", GameRules.Category.MISC, GameRules.BooleanValue.create(true)
@@ -245,6 +_,7 @@
     );
     private final Map<GameRules.Key<?>, GameRules.Value<?>> rules;
     private final FeatureFlagSet enabledFeatures;
+    private final GameRules.Value<?>[] gameruleArray; // Paper - Perf: Use array for gamerule storage
 
     public static <T extends GameRules.Value<T>> GameRules.Type<T> getType(GameRules.Key<T> key) {
         return (GameRules.Type<T>)GAME_RULE_TYPES.get(key);
@@ -292,10 +_,21 @@
     private GameRules(Map<GameRules.Key<?>, GameRules.Value<?>> rules, FeatureFlagSet enabledFeatures) {
         this.rules = rules;
         this.enabledFeatures = enabledFeatures;
+
+        // Paper start - Perf: Use array for gamerule storage
+        int arraySize = GameRules.Key.lastGameRuleIndex + 1;
+        GameRules.Value<?>[] values = new GameRules.Value[arraySize];
+
+        for (Entry<GameRules.Key<?>, GameRules.Value<?>> entry : rules.entrySet()) {
+            values[entry.getKey().gameRuleIndex] = entry.getValue();
+        }
+
+        this.gameruleArray = values;
+        // Paper end - Perf: Use array for gamerule storage
     }
 
     public <T extends GameRules.Value<T>> T getRule(GameRules.Key<T> key) {
-        T value = (T)this.rules.get(key);
+        T value = key == null ? null : (T) this.gameruleArray[key.gameRuleIndex]; // Paper - Perf: Use array for gamerule storage
         if (value == null) {
             throw new IllegalArgumentException("Tried to access invalid game rule");
         } else {
@@ -336,13 +_,13 @@
         }
     }
 
-    public void assignFrom(GameRules rules, @Nullable MinecraftServer server) {
-        rules.rules.keySet().forEach(key -> this.assignCap((GameRules.Key<?>)key, rules, server));
+    public void assignFrom(GameRules rules, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
+        rules.rules.keySet().forEach(key -> this.assignCap((GameRules.Key<?>)key, rules, level)); // CraftBukkit - per-world
     }
 
-    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> key, GameRules rules, @Nullable MinecraftServer server) {
+    private <T extends GameRules.Value<T>> void assignCap(GameRules.Key<T> key, GameRules rules, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
         T rule = rules.getRule(key);
-        this.<T>getRule(key).setFrom(rule, server);
+        this.<T>getRule(key).setFrom(rule, level); // CraftBukkit - per-world
     }
 
     public boolean getBoolean(GameRules.Key<GameRules.BooleanValue> key) {
@@ -357,7 +_,7 @@
         private boolean value;
 
         private static GameRules.Type<GameRules.BooleanValue> create(
-            boolean defaultValue, BiConsumer<MinecraftServer, GameRules.BooleanValue> changeListener, FeatureFlagSet requiredFeatures
+            boolean defaultValue, BiConsumer<net.minecraft.server.level.ServerLevel, GameRules.BooleanValue> changeListener, FeatureFlagSet requiredFeatures // Paper - per world gamerules
         ) {
             return new GameRules.Type<>(
                 BoolArgumentType::bool,
@@ -369,7 +_,7 @@
             );
         }
 
-        static GameRules.Type<GameRules.BooleanValue> create(boolean defaultValue, BiConsumer<MinecraftServer, GameRules.BooleanValue> changeListener) {
+        static GameRules.Type<GameRules.BooleanValue> create(boolean defaultValue, BiConsumer<net.minecraft.server.level.ServerLevel, GameRules.BooleanValue> changeListener) { // Paper - per world gamerules
             return new GameRules.Type<>(
                 BoolArgumentType::bool,
                 type -> new GameRules.BooleanValue(type, defaultValue),
@@ -390,17 +_,21 @@
         }
 
         @Override
-        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
-            this.value = BoolArgumentType.getBool(context, paramName);
+        // Paper start - Add WorldGameRuleChangeEvent
+        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<BooleanValue> gameRuleKey) {
+            io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(BoolArgumentType.getBool(context, paramName)));
+            if (!event.callEvent()) return;
+            this.value = Boolean.parseBoolean(event.getValue());
+        // Paper end - Add WorldGameRuleChangeEvent
         }
 
         public boolean get() {
             return this.value;
         }
 
-        public void set(boolean value, @Nullable MinecraftServer server) {
+        public void set(boolean value, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
             this.value = value;
-            this.onChanged(server);
+            this.onChanged(level); // CraftBukkit - per-world
         }
 
         @Override
@@ -409,7 +_,7 @@
         }
 
         @Override
-        protected void deserialize(String value) {
+        public void deserialize(String value) {
             this.value = Boolean.parseBoolean(value);
         }
 
@@ -429,9 +_,9 @@
         }
 
         @Override
-        public void setFrom(GameRules.BooleanValue value, @Nullable MinecraftServer server) {
+        public void setFrom(GameRules.BooleanValue value, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
             this.value = value.value;
-            this.onChanged(server);
+            this.onChanged(level); // CraftBukkit - per-world
         }
     }
 
@@ -469,7 +_,7 @@
     public static class IntegerValue extends GameRules.Value<GameRules.IntegerValue> {
         private int value;
 
-        private static GameRules.Type<GameRules.IntegerValue> create(int defaultValue, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeListener) {
+        private static GameRules.Type<GameRules.IntegerValue> create(int defaultValue, BiConsumer<net.minecraft.server.level.ServerLevel, GameRules.IntegerValue> changeListener) { // CraftBukkit - per-world
             return new GameRules.Type<>(
                 IntegerArgumentType::integer,
                 type -> new GameRules.IntegerValue(type, defaultValue),
@@ -481,7 +_,7 @@
         }
 
         static GameRules.Type<GameRules.IntegerValue> create(
-            int defaultValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<MinecraftServer, GameRules.IntegerValue> changeListener
+            int defaultValue, int min, int max, FeatureFlagSet requiredFeatures, BiConsumer<net.minecraft.server.level.ServerLevel, GameRules.IntegerValue> changeListener // CraftBukkit - per-world
         ) {
             return new GameRules.Type<>(
                 () -> IntegerArgumentType.integer(min, max),
@@ -503,17 +_,21 @@
         }
 
         @Override
-        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
-            this.value = IntegerArgumentType.getInteger(context, paramName);
+        // Paper start - Add WorldGameRuleChangeEvent
+        protected void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<IntegerValue> gameRuleKey) {
+            io.papermc.paper.event.world.WorldGameRuleChangeEvent event = new io.papermc.paper.event.world.WorldGameRuleChangeEvent(context.getSource().getBukkitWorld(), context.getSource().getBukkitSender(), org.bukkit.GameRule.getByName(gameRuleKey.toString()), String.valueOf(IntegerArgumentType.getInteger(context, paramName)));
+            if (!event.callEvent()) return;
+            this.value = Integer.parseInt(event.getValue());
+        // Paper end - Add WorldGameRuleChangeEvent
         }
 
         public int get() {
             return this.value;
         }
 
-        public void set(int value, @Nullable MinecraftServer server) {
+        public void set(int value, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
             this.value = value;
-            this.onChanged(server);
+            this.onChanged(level); // CraftBukkit - per-world
         }
 
         @Override
@@ -522,7 +_,7 @@
         }
 
         @Override
-        protected void deserialize(String value) {
+        public void deserialize(String value) {
             this.value = safeParse(value);
         }
 
@@ -564,13 +_,17 @@
         }
 
         @Override
-        public void setFrom(GameRules.IntegerValue value, @Nullable MinecraftServer server) {
+        public void setFrom(GameRules.IntegerValue value, @Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
             this.value = value.value;
-            this.onChanged(server);
+            this.onChanged(level); // CraftBukkit - per-world
         }
     }
 
     public static final class Key<T extends GameRules.Value<T>> {
+        // Paper start - Perf: Use array for gamerule storage
+        public static int lastGameRuleIndex = 0;
+        public final int gameRuleIndex = lastGameRuleIndex++;
+        // Paper end - Perf: Use array for gamerule storage
         final String id;
         private final GameRules.Category category;
 
@@ -610,7 +_,7 @@
     public static class Type<T extends GameRules.Value<T>> {
         final Supplier<ArgumentType<?>> argument;
         private final Function<GameRules.Type<T>, T> constructor;
-        final BiConsumer<MinecraftServer, T> callback;
+        final BiConsumer<net.minecraft.server.level.ServerLevel, T> callback; // CraftBukkit - per-world
         private final GameRules.VisitorCaller<T> visitorCaller;
         final Class<T> valueClass;
         final FeatureFlagSet requiredFeatures;
@@ -618,7 +_,7 @@
         Type(
             Supplier<ArgumentType<?>> argument,
             Function<GameRules.Type<T>, T> constructor,
-            BiConsumer<MinecraftServer, T> callback,
+            BiConsumer<net.minecraft.server.level.ServerLevel, T> callback, // CraftBukkit - per-world
             GameRules.VisitorCaller<T> visitorCaller,
             Class<T> valueClass,
             FeatureFlagSet requiredFeatures
@@ -646,6 +_,12 @@
         public FeatureFlagSet requiredFeatures() {
             return this.requiredFeatures;
         }
+        // Paper start - expose FeatureElement wrapper for GameRules.Type.
+        // Chosen over simply adding this to the inheritance to avoid reobf issues with spigot...
+        public net.minecraft.world.flag.FeatureElement asFeatureElement() {
+            return GameRules.Type.this::requiredFeatures;
+        }
+        // Paper end - expose FeatureElement wrapper for GameRules.Type.
     }
 
     public abstract static class Value<T extends GameRules.Value<T>> {
@@ -655,20 +_,20 @@
             this.type = type;
         }
 
-        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName);
+        protected abstract void updateFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<T> gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
 
-        public void setFromArgument(CommandContext<CommandSourceStack> context, String paramName) {
-            this.updateFromArgument(context, paramName);
-            this.onChanged(context.getSource().getServer());
+        public void setFromArgument(CommandContext<CommandSourceStack> context, String paramName, GameRules.Key<T> gameRuleKey) { // Paper - Add WorldGameRuleChangeEvent
+            this.updateFromArgument(context, paramName, gameRuleKey); // Paper - Add WorldGameRuleChangeEvent
+            this.onChanged(context.getSource().getLevel()); // CraftBukkit - per-world
         }
 
-        protected void onChanged(@Nullable MinecraftServer server) {
-            if (server != null) {
-                this.type.callback.accept(server, this.getSelf());
+        public void onChanged(@Nullable net.minecraft.server.level.ServerLevel level) { // CraftBukkit - per-world
+            if (level != null) { // CraftBukkit - per-world
+                this.type.callback.accept(level, this.getSelf()); // CraftBukkit - per-world
             }
         }
 
-        protected abstract void deserialize(String value);
+        public abstract void deserialize(String value);
 
         public abstract String serialize();
 
@@ -683,7 +_,7 @@
 
         protected abstract T copy();
 
-        public abstract void setFrom(T value, @Nullable MinecraftServer server);
+        public abstract void setFrom(T value, @Nullable net.minecraft.server.level.ServerLevel level); // CraftBukkit - per-world
     }
 
     interface VisitorCaller<T extends GameRules.Value<T>> {
