9
0
mirror of https://github.com/Xiao-MoMi/Custom-Crops.git synced 2026-01-06 15:51:52 +00:00

force tick

This commit is contained in:
XiaoMoMi
2024-09-02 00:31:29 +08:00
parent ab6d8527ec
commit 3f98c117fc
24 changed files with 278 additions and 49 deletions

View File

@@ -38,6 +38,7 @@ public class MappedRegistry<K, T> implements WriteableRegistry<K, T> {
public void register(K key, T value) {
byKey.put(key, value);
byValue.put(value, key);
byID.add(value);
}
@Override
@@ -67,6 +68,16 @@ public class MappedRegistry<K, T> implements WriteableRegistry<K, T> {
return byKey.get(key);
}
@Override
public boolean containsKey(@Nullable K key) {
return byKey.containsKey(key);
}
@Override
public boolean containsValue(@Nullable T value) {
return byValue.containsKey(value);
}
@NotNull
@Override
public Iterator<T> iterator() {

View File

@@ -30,4 +30,8 @@ public interface Registry<K, T> extends IdMap<T> {
@Nullable
T get(@Nullable K key);
boolean containsKey(@Nullable K key);
boolean containsValue(@Nullable T value);
}

View File

@@ -153,6 +153,11 @@ public class CropBlock extends AbstractCustomCropsBlock {
fixOrGetState(world, pos3, event.placedID());
}
@Override
public boolean isBlockInstance(String id) {
return Registries.STAGE_TO_CROP_UNSAFE.containsKey(id);
}
@Override
public void onInteract(WrappedInteractEvent event) {
final Player player = event.player();
@@ -287,8 +292,8 @@ public class CropBlock extends AbstractCustomCropsBlock {
id(state, cropConfig.id());
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
return state;

View File

@@ -52,7 +52,7 @@ public class CrowAttack {
}
}
this.viewers = viewers.toArray(new Player[0]);
this.cropLocation = location.clone().add(RandomUtils.generateRandomDouble(-0.25, 0.25), 0, RandomUtils.generateRandomDouble(-0.25, 0.25));
this.cropLocation = LocationUtils.toBlockCenterLocation(location).add(RandomUtils.generateRandomDouble(-0.25, 0.25), 0, RandomUtils.generateRandomDouble(-0.25, 0.25));
float yaw = RandomUtils.generateRandomInt(-180, 180);
this.cropLocation.setYaw(yaw);
this.flyModel = flyModel;
@@ -66,47 +66,39 @@ public class CrowAttack {
public void start() {
if (this.viewers.length == 0) return;
FakeArmorStand fake1 = SparrowHeart.getInstance().createFakeArmorStand(dynamicLocation);
fake1.invisible(true);
fake1.small(true);
fake1.equipment(EquipmentSlot.HEAD, flyModel);
FakeArmorStand fake2 = SparrowHeart.getInstance().createFakeArmorStand(cropLocation);
fake1.invisible(true);
fake1.small(true);
fake1.equipment(EquipmentSlot.HEAD, standModel);
FakeArmorStand fake = SparrowHeart.getInstance().createFakeArmorStand(dynamicLocation);
fake.invisible(true);
fake.small(true);
fake.equipment(EquipmentSlot.HEAD, flyModel);
for (Player player : this.viewers) {
fake1.spawn(player);
fake.spawn(player);
}
this.task = BukkitCustomCropsPlugin.getInstance().getScheduler().asyncRepeating(() -> {
timer++;
if (timer < 100) {
dynamicLocation.add(vectorDown);
for (Player player : this.viewers) {
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake1.entityID());
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake.entityID());
}
} else if (timer == 100){
} else if (timer == 100) {
fake.equipment(EquipmentSlot.HEAD, standModel);
for (Player player : this.viewers) {
fake1.destroy(player);
}
for (Player player : this.viewers) {
fake2.spawn(player);
fake.updateEquipment(player);
}
} else if (timer == 150) {
fake.equipment(EquipmentSlot.HEAD, flyModel);
for (Player player : this.viewers) {
fake2.destroy(player);
}
for (Player player : this.viewers) {
fake1.spawn(player);
fake.updateEquipment(player);
}
} else if (timer > 150) {
dynamicLocation.add(vectorUp);
for (Player player : this.viewers) {
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake1.entityID());
SparrowHeart.getInstance().sendClientSideTeleportEntity(player, dynamicLocation, false, fake.entityID());
}
}
if (timer > 300) {
if (timer > 250) {
for (Player player : this.viewers) {
fake1.destroy(player);
fake.destroy(player);
}
task.cancel();
}

View File

@@ -43,4 +43,6 @@ public interface CustomCropsBlock {
void onBreak(WrappedBreakEvent event);
void onPlace(WrappedPlaceEvent event);
boolean isBlockInstance(String id);
}

View File

@@ -90,12 +90,17 @@ public class GreenhouseBlock extends AbstractCustomCropsBlock {
CustomCropsWorld<?> world = event.world();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
}
@Override
public boolean isBlockInstance(String id) {
return ConfigManager.greenhouse().contains(id);
}
public CustomCropsBlockState getOrFixState(CustomCropsWorld<?> world, Pos3 pos3) {
Optional<CustomCropsBlockState> optional = world.getBlockState(pos3);
if (optional.isPresent() && optional.get().type() instanceof GreenhouseBlock) {
@@ -104,8 +109,8 @@ public class GreenhouseBlock extends AbstractCustomCropsBlock {
CustomCropsBlockState state = createBlockState();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
return state;

View File

@@ -199,13 +199,18 @@ public class PotBlock extends AbstractCustomCropsBlock {
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
ActionManager.trigger(context, config.placeActions());
}
@Override
public boolean isBlockInstance(String id) {
return Registries.ITEM_TO_POT.containsKey(id);
}
@Override
public void onInteract(WrappedInteractEvent event) {
PotConfig potConfig = Registries.ITEM_TO_POT.get(event.relatedID());
@@ -291,8 +296,8 @@ public class PotBlock extends AbstractCustomCropsBlock {
water(state, potConfig.isWet(blockID) ? 1 : 0);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
return state;

View File

@@ -90,12 +90,17 @@ public class ScarecrowBlock extends AbstractCustomCropsBlock {
CustomCropsWorld<?> world = event.world();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
}
@Override
public boolean isBlockInstance(String id) {
return ConfigManager.scarecrow().contains(id);
}
public CustomCropsBlockState getOrFixState(CustomCropsWorld<?> world, Pos3 pos3) {
Optional<CustomCropsBlockState> optional = world.getBlockState(pos3);
if (optional.isPresent() && optional.get().type() instanceof ScarecrowBlock) {
@@ -104,8 +109,8 @@ public class ScarecrowBlock extends AbstractCustomCropsBlock {
CustomCropsBlockState state = createBlockState();
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at pos3[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
return state;

View File

@@ -137,6 +137,11 @@ public class SprinklerBlock extends AbstractCustomCropsBlock {
ActionManager.trigger(context, config.placeActions());
}
@Override
public boolean isBlockInstance(String id) {
return Registries.ITEM_TO_SPRINKLER.containsKey(id);
}
@Override
public void onInteract(WrappedInteractEvent event) {
SprinklerConfig config = Registries.ITEM_TO_SPRINKLER.get(event.relatedID());
@@ -225,8 +230,8 @@ public class SprinklerBlock extends AbstractCustomCropsBlock {
water(state, blockID.equals(sprinklerConfig.threeDItemWithWater()) ? 1 : 0);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
return state;

View File

@@ -118,8 +118,8 @@ public class SeedItem extends AbstractCustomCropsItem {
cropBlock.point(state, point);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});

View File

@@ -111,8 +111,8 @@ public class SprinklerItem extends AbstractCustomCropsItem {
BukkitCustomCropsPlugin.getInstance().getItemManager().place(LocationUtils.toSurfaceCenterLocation(targetLocation), config.existenceForm(), config.threeDItem(), FurnitureRotation.NONE);
world.addBlockState(pos3, state).ifPresent(previous -> {
BukkitCustomCropsPlugin.getInstance().debug(
"Overwrite old data with " + state.compoundMap().toString() +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous.compoundMap().toString()
"Overwrite old data with " + state +
" at location[" + world.worldName() + "," + pos3 + "] which used to be " + previous
);
});
ActionManager.trigger(context, config.placeActions());

View File

@@ -179,7 +179,7 @@ public interface CustomCropsChunk {
CustomCropsSection getSection(int sectionID);
Collection<CustomCropsSection> sections();
CustomCropsSection[] sections();
Optional<CustomCropsSection> removeSection(int sectionID);

View File

@@ -257,8 +257,8 @@ public class CustomCropsChunkImpl implements CustomCropsChunk {
}
@Override
public Collection<CustomCropsSection> sections() {
return loadedSections.values();
public CustomCropsSection[] sections() {
return loadedSections.values().toArray(new CustomCropsSection[0]);
}
@Override

View File

@@ -80,6 +80,8 @@ public interface CustomCropsWorld<W> {
int getChunkBlockAmount(Pos3 pos3, Class<? extends CustomCropsBlock> clazz);
CustomCropsChunk[] loadedChunks();
/**
* Get the state of the block at a certain location
*

View File

@@ -131,6 +131,11 @@ public class CustomCropsWorldImpl<W> implements CustomCropsWorld<W> {
}
}
@Override
public CustomCropsChunk[] loadedChunks() {
return loadedChunks.values().toArray(new CustomCropsChunk[0]);
}
@NotNull
@Override
public Optional<CustomCropsBlockState> getBlockState(Pos3 location) {

View File

@@ -43,4 +43,7 @@ public interface MessageConstants {
TranslatableComponent.Builder COMMAND_SET_DATE_FAILURE_REFERENCE = Component.translatable().key("command.date.set.failure.reference");
TranslatableComponent.Builder COMMAND_SET_DATE_FAILURE_OTHER = Component.translatable().key("command.date.set.failure.other");
TranslatableComponent.Builder COMMAND_SET_DATE_FAILURE_INVALID = Component.translatable().key("command.date.set.failure.invalid");
TranslatableComponent.Builder COMMAND_FORCE_TICK_SUCCESS = Component.translatable().key("command.force_tick.success");
TranslatableComponent.Builder COMMAND_FORCE_TICK_FAILURE_TYPE = Component.translatable().key("command.force_tick.failure.type");
TranslatableComponent.Builder COMMAND_FORCE_TICK_FAILURE_DISABLE = Component.translatable().key("command.force_tick.failure.disable");
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.common.util;
public interface QuadConsumer<K, V, S, O> {
void accept(K k, V v, S s, O o);
}

View File

@@ -15,7 +15,7 @@ asm_commons_version=9.7
jar_relocator_version=1.7
adventure_bundle_version=4.17.0
adventure_platform_version=4.3.4
sparrow_heart_version=0.39
sparrow_heart_version=0.40
cloud_core_version=2.0.0-rc.2
cloud_services_version=2.0.0-rc.2
cloud_brigadier_version=2.0.0-beta.9

View File

@@ -40,7 +40,8 @@ public class BukkitCommandManager extends AbstractCommandManager<CommandSender>
new GetSeasonCommand(this),
new SetSeasonCommand(this),
new GetDateCommand(this),
new SetDateCommand(this)
new SetDateCommand(this),
new ForceTickCommand(this)
);
private final Index<String, CommandFeature<CommandSender>> INDEX = Index.create(CommandFeature::getFeatureID, FEATURES);

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) <2024> <XiaoMoMi>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.momirealms.customcrops.bukkit.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.customcrops.api.BukkitCustomCropsPlugin;
import net.momirealms.customcrops.api.core.Registries;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.world.*;
import net.momirealms.customcrops.bukkit.command.BukkitCommandFeature;
import net.momirealms.customcrops.common.command.CustomCropsCommandManager;
import net.momirealms.customcrops.common.locale.MessageConstants;
import net.momirealms.customcrops.common.util.Key;
import net.momirealms.customcrops.common.util.QuadConsumer;
import net.momirealms.customcrops.common.util.TriConsumer;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.bukkit.parser.NamespacedKeyParser;
import org.incendo.cloud.bukkit.parser.WorldParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.EnumParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class ForceTickCommand extends BukkitCommandFeature<CommandSender> {
public ForceTickCommand(CustomCropsCommandManager<CommandSender> commandManager) {
super(commandManager);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("world", WorldParser.worldParser())
.required("type", NamespacedKeyParser.namespacedKeyComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
int all = Registries.BLOCK.size();
ArrayList<CustomCropsBlock> blocks = new ArrayList<>();
for (int i = 0; i < all; i++) {
blocks.add(Registries.BLOCK.byId(i));
}
return CompletableFuture.completedFuture(
blocks.stream().map(block -> Suggestion.suggestion(block.type().asString())).toList()
);
}
}))
.required("mode", EnumParser.enumParser(Mode.class))
.flag(manager.flagBuilder("silent").build())
.handler(context -> {
World world = context.get("world");
NamespacedKey type = context.get("type");
Mode mode = context.get("mode");
Key key = Key.key(type.asString());
CustomCropsBlock customCropsBlock = Registries.BLOCK.get(key);
if (customCropsBlock == null) {
handleFeedback(context.sender(), MessageConstants.COMMAND_FORCE_TICK_FAILURE_TYPE, Component.text(key.asString()));
return;
}
Optional<CustomCropsWorld<?>> optionalWorld = BukkitCustomCropsPlugin.getInstance().getWorldManager().getWorld(world);
if (optionalWorld.isEmpty()) {
handleFeedback(context.sender(), MessageConstants.COMMAND_FORCE_TICK_FAILURE_DISABLE, Component.text(world.getName()));
return;
}
CustomCropsWorld<?> customCropsWorld = optionalWorld.get();
BukkitCustomCropsPlugin.getInstance().getScheduler().async().execute(() -> {
int amount = 0;
long time1 = System.currentTimeMillis();
for (CustomCropsChunk customCropsChunk : customCropsWorld.loadedChunks()) {
for (CustomCropsSection customCropsSection : customCropsChunk.sections()) {
for (Map.Entry<BlockPos, CustomCropsBlockState> entry : customCropsSection.blockMap().entrySet()) {
CustomCropsBlockState state = entry.getValue();
if (state.type() == customCropsBlock) {
Pos3 pos3 = entry.getKey().toPos3(customCropsChunk.chunkPos());
mode.consumer.accept(customCropsBlock, state, customCropsWorld, pos3);
amount++;
}
}
}
}
handleFeedback(context.sender(), MessageConstants.COMMAND_FORCE_TICK_SUCCESS, Component.text(System.currentTimeMillis() - time1), Component.text(amount));
});
});
}
private enum Mode {
RANDOM_TICK(CustomCropsBlock::randomTick),
SCHEDULED_TICK(CustomCropsBlock::scheduledTick),
ALL((b, s, w, p) -> {
b.randomTick(s, w, p);
b.scheduledTick(s, w, p);
});
private final QuadConsumer<CustomCropsBlock, CustomCropsBlockState, CustomCropsWorld<?>, Pos3> consumer;
Mode(QuadConsumer<CustomCropsBlock, CustomCropsBlockState, CustomCropsWorld<?>, Pos3> consumer) {
this.consumer = consumer;
}
}
@Override
public String getFeatureID() {
return "force_tick";
}
}

View File

@@ -24,7 +24,9 @@ import net.momirealms.customcrops.api.core.*;
import net.momirealms.customcrops.api.core.block.BreakReason;
import net.momirealms.customcrops.api.core.block.CustomCropsBlock;
import net.momirealms.customcrops.api.core.item.CustomCropsItem;
import net.momirealms.customcrops.api.core.world.CustomCropsBlockState;
import net.momirealms.customcrops.api.core.world.CustomCropsWorld;
import net.momirealms.customcrops.api.core.world.Pos3;
import net.momirealms.customcrops.api.core.wrapper.WrappedBreakEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractAirEvent;
import net.momirealms.customcrops.api.core.wrapper.WrappedInteractEvent;
@@ -454,8 +456,23 @@ public class BukkitItemManager extends AbstractItemManager {
return;
}
String itemID = id(itemInHand);
CustomCropsWorld<?> world = optionalWorld.get();
Pos3 pos3 = Pos3.from(location);
Optional<CustomCropsBlockState> optionalState = world.getBlockState(pos3);
if (optionalState.isPresent()) {
CustomCropsBlockState customCropsBlockState = optionalState.get();
String anyID = anyID(location);
System.out.println(anyID);
if (!customCropsBlockState.type().isBlockInstance(anyID)) {
world.removeBlockState(pos3);
plugin.debug("[" + location.getWorld().getName() + "] Removed inconsistent block data at " + pos3 + " which used to be " + customCropsBlockState);
} else {
event.setCancelled(true);
return;
}
}
String itemID = id(itemInHand);
WrappedPlaceEvent wrapped = new WrappedPlaceEvent(player, world, location, placedID, hand, itemInHand, itemID, event);
CustomCropsBlock customCropsBlock = Registries.BLOCKS.get(placedID);
if (customCropsBlock != null) {

View File

@@ -50,3 +50,10 @@ debug_data:
usage:
- /customcrops debug data
- /ccrops debug data
force_tick:
enable: true
permission: customcrops.command.force_tick
usage:
- /customcrops force-tick
- /ccrops force-tick

View File

@@ -54,6 +54,9 @@ command.date.set.failure.disable: "<red>Date is disabled in world [<arg:0>]</red
command.date.set.failure.reference: "<red>World [<arg:0>] is not the reference world</red>"
command.date.set.failure.other: "<red>Can't set date for world [<arg:0>] because plugin [<arg:1>] takes over the calendar</red>"
command.date.set.failure.invalid: "<red>Invalid date [<arg:1>]</red>"
command.force_tick.success: "<white>Took <arg:0>ms ticking <arg:1> blocks</white>"
command.force_tick.failure.disable: "<red>CustomCrops is not enabled in world [<arg:0>]</red>"
command.force_tick.failure.type: "<red>Unknown type [<arg:0>]</red>"
season.spring: "Spring"
season.summer: "Summer"
season.autumn: "Autumn"

View File

@@ -53,6 +53,9 @@ command.date.set.success: "<white>成功设置世界 [<arg:0>] 的日期为 [<ar
command.date.set.failure.reference: "<red>世界 [<arg:0>] 不是同步季节设置的参考世界</red>"
command.date.set.failure.other: "<red>无法设置世界 [<arg:0>] 的日期,原因是插件 [<arg:1>] 接管了日历</red>"
command.date.set.failure.invalid: "<red>无效的日期 [<arg:1>]</red>"
command.force_tick.success: "<white>花费 <arg:0>ms 更新了 <arg:1> 个方块</white>"
command.force_tick.failure.disable: "<red>CustomCrops没有在世界 [<arg:0>] 启用</red>"
command.force_tick.failure.type: "<red>未知的类型 [<arg:0>]</red>"
season.spring: "春"
season.summer: "夏"
season.autumn: "秋"