├── gradle └── wrapper │ ├── gradle-wrapper.properties │ └── gradle-wrapper.jar ├── src └── main │ ├── resources │ ├── assets │ │ └── subtick │ │ │ └── icon.png │ ├── subtick.accesswidener │ ├── subtick.mixins.json │ └── fabric.mod.json │ └── java │ └── subtick │ ├── client │ └── SubtickClient.java │ ├── SubTickSettings.java │ ├── mixins │ ├── carpet │ │ └── DataBuilderMixin.java │ ├── BlockEntityTickerMixin.java │ ├── world_tickphases │ │ ├── BlockEntityWorldMixin.java │ │ ├── RaidsWorldMixin.java │ │ ├── EntityWorldMixin.java │ │ ├── TileTicksWorldMixin.java │ │ └── BlockEventWorldMixin.java │ ├── PistonBlockMixin.java │ ├── ServerChunkManagerMixin.java │ ├── EntityListMixin.java │ ├── MinecraftServerMixin.java │ └── WorldTickSchedulerMixin.java │ ├── commands │ ├── WhenCommand.java │ ├── HighlightCommand.java │ ├── TTCommand.java │ └── BECommand.java │ ├── SubTick.java │ ├── progress │ ├── TickActions.java │ └── TickProgress.java │ └── Highlights.java ├── settings.gradle ├── gradle.properties ├── LICENSE ├── README.md ├── .gitignore ├── gradlew.bat └── gradlew /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiraagChakravarthy/SubTick/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/assets/subtick/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiraagChakravarthy/SubTick/HEAD/src/main/resources/assets/subtick/icon.png -------------------------------------------------------------------------------- /src/main/java/subtick/client/SubtickClient.java: -------------------------------------------------------------------------------- 1 | package subtick.client; 2 | 3 | @net.fabricmc.api.Environment(net.fabricmc.api.EnvType.CLIENT) 4 | public class SubtickClient { 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/subtick.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | accessible field net/minecraft/server/world/ServerWorld syncedBlockEventQueue Lit/unimi/dsi/fastutil/objects/ObjectLinkedOpenHashSet; 4 | accessible method net/minecraft/entity/FallingBlockEntity (Lnet/minecraft/world/World;DDDLnet/minecraft/block/BlockState;)V 5 | accessible field net/minecraft/world/tick/WorldTickScheduler tickableTicks Ljava/util/Queue; -------------------------------------------------------------------------------- /src/main/java/subtick/SubTickSettings.java: -------------------------------------------------------------------------------- 1 | package subtick; 2 | 3 | import carpet.settings.Rule; 4 | 5 | public class SubTickSettings { 6 | @Rule(desc = "tells carpet clients the game isn't frozen, allowing piston animations to play immediately", category = "SUBTICK") 7 | public static boolean vanillaPistonAnimations = false; 8 | 9 | @Rule(desc = "changes one line of code that makes lossless bedrock breaking possible again (barely)", category = "SUBTICK") 10 | public static boolean losslessBedrockBreaking = false; 11 | 12 | @Rule(desc = "skip stepping over events executed on the wrong block", category = "SUBTICK") 13 | public static boolean skipInvalidEvents = true; 14 | } -------------------------------------------------------------------------------- /src/main/resources/subtick.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "subtick.mixins", 4 | "compatibilityLevel": "JAVA_17", 5 | "mixins": [ 6 | "BlockEntityTickerMixin", 7 | "EntityListMixin", 8 | "MinecraftServerMixin", 9 | "PistonBlockMixin", 10 | "ServerChunkManagerMixin", 11 | "WorldTickSchedulerMixin", 12 | "carpet.DataBuilderMixin", 13 | "world_tickphases.BlockEntityWorldMixin", 14 | "world_tickphases.BlockEventWorldMixin", 15 | "world_tickphases.EntityWorldMixin", 16 | "world_tickphases.RaidsWorldMixin", 17 | "world_tickphases.TileTicksWorldMixin" 18 | ], 19 | "injectors": { 20 | "defaultRequire": 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/use or https://modmuss50.me/fabric.html 6 | display_minecraft_version=1.18.x 7 | minecraft_version=1.18.2 8 | yarn_mappings=1.18.2+build.1 9 | loader_version=0.12.12 10 | carpet_minecraft_version=1.18.2 11 | # check available versions on maven for the given minecraft version you are using 12 | carpet_core_version=1.4.66+v220228 13 | 14 | # Mod Properties 15 | mod_version = 2.0 beta 16 | maven_group = subtick 17 | archives_base_name = subtick 18 | 19 | # Dependencies 20 | # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric 21 | # fabric_version=0.3.0+build.187 22 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "subtick", 4 | "version": "${version}", 5 | 6 | 7 | "name": "SubTick", 8 | "description": "for when you're trying to do more than one thing in a tick", 9 | "authors": [ 10 | "Punchster2" 11 | ], 12 | 13 | "license": "CC0-1.0", 14 | 15 | "environment": "*", 16 | "accessWidener": "subtick.accesswidener", 17 | 18 | "entrypoints": { 19 | "main": [ 20 | "subtick.SubTick" 21 | ] 22 | }, 23 | 24 | 25 | "mixins": [ 26 | "subtick.mixins.json" 27 | ], 28 | 29 | "depends": { 30 | "minecraft": "1.18.x", 31 | "fabricloader": ">=0.11.3", 32 | "carpet": ">=1.4.40" 33 | }, 34 | "custom": { 35 | "modmenu": { 36 | "parent": "carpet" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/carpet/DataBuilderMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.carpet; 2 | 3 | import net.minecraft.nbt.NbtCompound; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.Shadow; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 9 | import subtick.SubTickSettings; 10 | 11 | @Mixin(targets = "carpet.network.ServerNetworkHandler$DataBuilder") 12 | public class DataBuilderMixin { 13 | @Shadow private NbtCompound tag; 14 | 15 | @Inject(method = "withFrozenState", at=@At("TAIL"), remap = false) 16 | public void modifiedFrozenState(CallbackInfoReturnable cir){ 17 | if(SubTickSettings.vanillaPistonAnimations){ 18 | NbtCompound tickingState = new NbtCompound(); 19 | tickingState.putBoolean("is_paused", false); 20 | tickingState.putBoolean("deepFreeze", false); 21 | tag.put("TickingState", tickingState); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Punchster2, prgmTrouble 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Subtick 2 | 3 | Subtick is a carpet extension which lets you step through piston block events in the middle of a tick one by one. It ensures that all tick phases and dimensions are kept in order. 4 | 5 | ## Installation 6 | 7 | Download the jar file from the releases tab and add to your mods folder, along with fabric carpet 8 | 9 | ## Commands 10 | * be step: steps a single block event 11 | * bed step: steps through all block events in the current block event delay 12 | * be play: plays through block events on a given time interval 13 | * bed play: plays through entire block event delays on a given time interval 14 | * be count: says the number of block events currently in the queue 15 | * when: says which tick phase the game is currently in 16 | 17 | ## Carpet Rules 18 | * includeInvalidBlockEvents: step through block events executed on the wrong block type (these will never affect anything) 19 | * vanillaPistonAnimations: let moving block animations play to completion on carpet clients (only really works on servers) 20 | * highlightBlockEvents: show where block events take place to clients using glowing highlights 21 | * losslessBedrockBreaking: changes one line of code that makes lossless bedrock breaking possible again (not vanilla lol, false by default) 22 | * be radius: specifies the maximum distance away from the player block events will be stepped through. good on cmp servers with piston clocks at spawn. 23 | 24 | ## License 25 | [MIT](https://choosealicense.com/licenses/mit/) 26 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/BlockEntityTickerMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.block.entity.BlockEntity; 5 | import net.minecraft.world.chunk.BlockEntityTickInvoker; 6 | import org.spongepowered.asm.mixin.Final; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Shadow; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | import subtick.progress.TickProgress; 13 | 14 | import static subtick.progress.TickProgress.RUN_COMPLETELY; 15 | import static subtick.progress.TickProgress.STEP_TO_FINISH; 16 | 17 | @Mixin(targets = {"net.minecraft.world.chunk.WorldChunk$DirectBlockEntityTickInvoker"}, priority = 999) 18 | public abstract class BlockEntityTickerMixin implements BlockEntityTickInvoker { 19 | 20 | @Shadow @Final private BlockEntity blockEntity; 21 | 22 | @Inject(method = "tick", at=@At( 23 | value = "INVOKE", 24 | target = "Lnet/minecraft/block/entity/BlockEntityTicker;tick(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/entity/BlockEntity;)V")) 25 | public void preTileEntity(CallbackInfo ci){ 26 | if(blockEntity.getWorld() != null && blockEntity.getWorld().isClient){ 27 | return; 28 | } 29 | int runStatus = TickProgress.runStatus(); 30 | TickSpeed.process_entities = runStatus == RUN_COMPLETELY || runStatus == STEP_TO_FINISH; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/subtick/commands/WhenCommand.java: -------------------------------------------------------------------------------- 1 | package subtick.commands; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import carpet.utils.Messenger; 5 | import com.mojang.brigadier.CommandDispatcher; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import subtick.progress.TickProgress; 10 | 11 | import static net.minecraft.server.command.CommandManager.literal; 12 | 13 | public class WhenCommand { 14 | public static void register(CommandDispatcher dispatcher){ 15 | dispatcher.register(CommandManager.literal("when").executes((c) -> commandWhen(c, false)).then( 16 | literal("verbose").executes((c) -> commandWhen(c, true)) 17 | ) 18 | ); 19 | } 20 | 21 | private static int commandWhen(CommandContext c, boolean verbose){ 22 | if(TickSpeed.process_entities){ 23 | Messenger.m(c.getSource(), "wi Game is not fr" + 24 | "ozen"); 25 | } else { 26 | if(verbose){ 27 | int tickPhase = TickProgress.getTickPhase(TickProgress.targetProgress); 28 | 29 | for (int i = 0; i < TickProgress.NUM_PHASES; i++) { 30 | Messenger.m(c.getSource(), "wi " + (i==tickPhase?"> ":"* ") + TickProgress.tickPhaseNamesPlural[i]); 31 | } 32 | } else { 33 | Messenger.m(c.getSource(), "gi Currently running " + TickProgress.progressName(TickProgress.targetProgress, true)); 34 | } 35 | } 36 | return 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/world_tickphases/BlockEntityWorldMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.world_tickphases; 2 | 3 | import net.minecraft.server.world.ServerWorld; 4 | import net.minecraft.util.profiler.Profiler; 5 | import net.minecraft.util.registry.RegistryEntry; 6 | import net.minecraft.util.registry.RegistryKey; 7 | import net.minecraft.world.MutableWorldProperties; 8 | import net.minecraft.world.StructureWorldAccess; 9 | import net.minecraft.world.World; 10 | import net.minecraft.world.dimension.DimensionType; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | import subtick.progress.TickProgress; 16 | 17 | import java.util.function.BooleanSupplier; 18 | import java.util.function.Supplier; 19 | 20 | @Mixin(value = ServerWorld.class, priority = 999) 21 | public abstract class BlockEntityWorldMixin extends World implements StructureWorldAccess { 22 | 23 | protected BlockEntityWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry registryEntry, Supplier profiler, boolean isClient, boolean debugWorld, long seed) { 24 | super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed); 25 | } 26 | 27 | @Inject(method="tick", at=@At(value="INVOKE", 28 | target = "Lnet/minecraft/server/world/ServerWorld;tickBlockEntities()V")) 29 | public void preBlockEntities(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 30 | TickProgress.update(TickProgress.TILE_ENTITIES, this.getRegistryKey()); 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/PistonBlockMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import net.minecraft.block.BlockState; 4 | import net.minecraft.block.FacingBlock; 5 | import net.minecraft.block.PistonBlock; 6 | import net.minecraft.state.property.DirectionProperty; 7 | import net.minecraft.util.math.BlockPos; 8 | import net.minecraft.util.math.Direction; 9 | import net.minecraft.world.World; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 16 | import subtick.SubTickSettings; 17 | 18 | import static net.minecraft.block.PistonBlock.EXTENDED; 19 | 20 | @Mixin(PistonBlock.class) 21 | public abstract class PistonBlockMixin extends FacingBlock { 22 | 23 | protected PistonBlockMixin(Settings settings) { 24 | super(settings); 25 | } 26 | 27 | @Shadow protected abstract boolean shouldExtend(World world, BlockPos pos, Direction pistonFace); 28 | 29 | @Inject(method = "onSyncedBlockEvent", at = @At( 30 | target = "Lnet/minecraft/block/PistonBlock;shouldExtend(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;)Z", 31 | value = "INVOKE")) 32 | public void modified(BlockState state, World world, BlockPos pos, int type, int data, CallbackInfoReturnable cir){ 33 | if(SubTickSettings.losslessBedrockBreaking) { 34 | Direction direction = state.get(FACING); 35 | boolean powered = shouldExtend(world, pos, direction); 36 | if (!powered && type == 0) { 37 | world.setBlockState(pos, state.with(EXTENDED, false), 2); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/world_tickphases/RaidsWorldMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.world_tickphases; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.server.world.ServerWorld; 5 | import net.minecraft.util.profiler.Profiler; 6 | import net.minecraft.util.registry.RegistryEntry; 7 | import net.minecraft.util.registry.RegistryKey; 8 | import net.minecraft.world.MutableWorldProperties; 9 | import net.minecraft.world.StructureWorldAccess; 10 | import net.minecraft.world.World; 11 | import net.minecraft.world.dimension.DimensionType; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | import subtick.progress.TickProgress; 17 | 18 | import java.util.function.BooleanSupplier; 19 | import java.util.function.Supplier; 20 | 21 | import static subtick.progress.TickProgress.*; 22 | 23 | @Mixin(value = ServerWorld.class, priority = 999) 24 | public abstract class RaidsWorldMixin extends World implements StructureWorldAccess { 25 | protected RaidsWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry registryEntry, Supplier profiler, boolean isClient, boolean debugWorld, long seed) { 26 | super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed); 27 | } 28 | 29 | 30 | @Inject(method="tick", at=@At( 31 | value="INVOKE", 32 | target = "Lnet/minecraft/village/raid/RaidManager;tick()V")) 33 | public void preRaids(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 34 | int runStatus = TickProgress.update(RAIDS, this.getRegistryKey()); 35 | TickSpeed.process_entities = runStatus == RUN_COMPLETELY || runStatus == STEP_TO_FINISH; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/ServerChunkManagerMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.server.world.ServerChunkManager; 5 | import net.minecraft.server.world.ServerWorld; 6 | import net.minecraft.world.chunk.ChunkManager; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | import subtick.progress.TickProgress; 14 | 15 | @Mixin(ServerChunkManager.class) 16 | public abstract class ServerChunkManagerMixin extends ChunkManager { 17 | 18 | @Shadow @Final private ServerWorld world; 19 | 20 | private boolean actuallyProcessEntities; 21 | 22 | @Inject(method = "tickChunks", at = @At( 23 | value = "INVOKE", 24 | target = "Lnet/minecraft/server/world/ServerWorld;isDebugWorld()Z", 25 | shift = At.Shift.BEFORE)) 26 | public void preTickChunks(CallbackInfo ci){ 27 | actuallyProcessEntities = TickSpeed.process_entities; 28 | 29 | int dimension = TickProgress.dim(this.world.getRegistryKey()); 30 | 31 | TickSpeed.process_entities = TickProgress.currentProgress == TickProgress.progressOf(TickProgress.RAIDS, dimension) 32 | && (TickProgress.runStatus() == TickProgress.RUN_COMPLETELY || TickProgress.runStatus() == TickProgress.STEP_TO_FINISH); 33 | } 34 | 35 | @Inject(method = "tickChunks", at = @At( 36 | value = "INVOKE", 37 | target = "Lnet/minecraft/server/world/ServerWorld;isDebugWorld()Z", 38 | shift = At.Shift.AFTER)) 39 | public void postTickChunks(CallbackInfo ci){ 40 | TickSpeed.process_entities = actuallyProcessEntities; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/EntityListMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 4 | import net.minecraft.entity.Entity; 5 | import net.minecraft.world.EntityList; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Shadow; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | import subtick.progress.TickProgress; 13 | 14 | import java.util.function.Consumer; 15 | 16 | import static subtick.progress.TickProgress.*; 17 | 18 | @Mixin(EntityList.class) 19 | public class EntityListMixin { 20 | @Shadow private @Nullable Int2ObjectMap iterating; 21 | 22 | @Shadow private Int2ObjectMap entities; 23 | 24 | @Inject(method = "forEach", at=@At("HEAD"), cancellable = true) 25 | public void entityStep(Consumer action, CallbackInfo ci){ 26 | if(entities.isEmpty() || entities.values().iterator().next().world.isClient){ 27 | iterating = null; 28 | return; 29 | } 30 | 31 | int runStatus = TickProgress.runStatus(); 32 | if(runStatus==RUN_COMPLETELY || runStatus==NO_RUN){ 33 | iterating = null; 34 | return; 35 | } 36 | ci.cancel(); 37 | if(runStatus==STEP_FROM_START){ 38 | this.iterating = entities; 39 | } 40 | 41 | //probably not possible for iterating to be null here 42 | try { 43 | for (Entity entity : this.iterating.values()) { 44 | action.accept(entity); 45 | } 46 | } catch (Throwable t){ 47 | if(iterating == null){ 48 | throw t; 49 | } 50 | if(targetProgress != STEP_TO_FINISH){ 51 | targetProgress++; 52 | TickProgress.update(ENTITIES, getDimension(targetProgress)); 53 | System.err.println("Stepped past " + progressName(targetProgress-1) + " due to entity crash"); 54 | } 55 | } 56 | 57 | if(runStatus==STEP_TO_FINISH){ 58 | iterating = null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | /run/ 120 | /build/ 121 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/MinecraftServerMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.server.MinecraftServer; 5 | import net.minecraft.server.ServerTask; 6 | import net.minecraft.server.command.CommandOutput; 7 | import net.minecraft.util.thread.ReentrantThreadExecutor; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | import subtick.Highlights; 13 | import subtick.progress.TickActions; 14 | import subtick.progress.TickProgress; 15 | 16 | import java.util.function.BooleanSupplier; 17 | 18 | @Mixin(MinecraftServer.class) 19 | public abstract class MinecraftServerMixin 20 | extends ReentrantThreadExecutor 21 | implements CommandOutput, AutoCloseable { 22 | 23 | public MinecraftServerMixin(String string) { 24 | super(string); 25 | } 26 | 27 | private boolean actuallyProcessEntities; 28 | 29 | @Inject(method = "tickWorlds", at=@At( 30 | value = "INVOKE", 31 | target = "Lnet/minecraft/server/function/CommandFunctionManager;tick()V", 32 | shift= At.Shift.AFTER)) 33 | public void preWorldsTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 34 | actuallyProcessEntities = TickSpeed.process_entities; 35 | if(TickSpeed.process_entities){ 36 | Highlights.clearHighlights((MinecraftServer) (Object)this); 37 | TickProgress.setTarget(TickProgress.POST_TICK); 38 | } 39 | TickActions.tick(); 40 | } 41 | 42 | @Inject(method = "tickWorlds", at=@At( 43 | target = "Lnet/minecraft/server/MinecraftServer;getNetworkIo()Lnet/minecraft/server/ServerNetworkIo;", 44 | value = "INVOKE", 45 | shift = At.Shift.BEFORE)) 46 | public void postWorldsTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 47 | TickProgress.update(TickProgress.POST_TICK); 48 | 49 | TickSpeed.process_entities = actuallyProcessEntities; 50 | if(actuallyProcessEntities){ 51 | TickProgress.setCurrent(TickProgress.PRE_TICK); 52 | TickProgress.setTarget(TickProgress.PRE_TICK); 53 | } 54 | TickActions.numActionsStep = 0; 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/subtick/SubTick.java: -------------------------------------------------------------------------------- 1 | package subtick; 2 | 3 | import carpet.CarpetExtension; 4 | import carpet.CarpetServer; 5 | import carpet.helpers.TickSpeed; 6 | import carpet.network.ServerNetworkHandler; 7 | import carpet.settings.SettingsManager; 8 | import com.mojang.brigadier.CommandDispatcher; 9 | import net.fabricmc.api.ModInitializer; 10 | import net.minecraft.network.MessageType; 11 | import net.minecraft.server.MinecraftServer; 12 | import net.minecraft.server.command.ServerCommandSource; 13 | import net.minecraft.server.command.TestCommand; 14 | import net.minecraft.server.world.ServerWorld; 15 | import net.minecraft.text.LiteralText; 16 | import subtick.commands.BECommand; 17 | import subtick.commands.TTCommand; 18 | import subtick.commands.WhenCommand; 19 | import subtick.progress.TickActions; 20 | import subtick.progress.TickProgress; 21 | 22 | import java.util.logging.Level; 23 | 24 | public class SubTick implements CarpetExtension, ModInitializer 25 | { 26 | @Override 27 | public String version() { 28 | return "subtick"; 29 | } 30 | 31 | @Override 32 | public void onInitialize() { 33 | CarpetServer.manageExtension(new SubTick()); 34 | } 35 | 36 | @Override 37 | public void onGameStarted() { 38 | CarpetServer.settingsManager.parseSettingsClass(SubTickSettings.class); 39 | SettingsManager.addGlobalRuleObserver((c, rule, state)->{ 40 | if(rule.name.equals("carpetClientFreeze")){ 41 | ServerNetworkHandler.updateFrozenStateToConnectedPlayers(); 42 | } 43 | }); 44 | } 45 | 46 | public static void say(ServerWorld world, String msg){ 47 | world.getServer().getPlayerManager().broadcast(new LiteralText(msg), MessageType.CHAT, null); 48 | } 49 | 50 | @Override 51 | public void onServerLoadedWorlds(MinecraftServer minecraftServer){ 52 | TickProgress.reset(); 53 | TickActions.reset(); 54 | Highlights.reset(); 55 | //A better way to check if carpetExtra is loaded 56 | } 57 | 58 | @Override 59 | public void registerCommands(CommandDispatcher dispatcher) { 60 | TTCommand.register(dispatcher); 61 | BECommand.register(dispatcher); 62 | WhenCommand.register(dispatcher); 63 | } 64 | 65 | @Override 66 | public void onTick(MinecraftServer server) { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/world_tickphases/EntityWorldMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.world_tickphases; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.entity.Entity; 5 | import net.minecraft.server.world.ServerWorld; 6 | import net.minecraft.util.profiler.Profiler; 7 | import net.minecraft.util.registry.RegistryEntry; 8 | import net.minecraft.util.registry.RegistryKey; 9 | import net.minecraft.world.MutableWorldProperties; 10 | import net.minecraft.world.StructureWorldAccess; 11 | import net.minecraft.world.World; 12 | import net.minecraft.world.dimension.DimensionType; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Desc; 16 | import org.spongepowered.asm.mixin.injection.Inject; 17 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 18 | import subtick.progress.TickProgress; 19 | 20 | import java.util.function.BooleanSupplier; 21 | import java.util.function.Supplier; 22 | 23 | import static subtick.progress.TickProgress.*; 24 | 25 | @Mixin(value = ServerWorld.class, priority = 999) 26 | public abstract class EntityWorldMixin extends World implements StructureWorldAccess { 27 | protected EntityWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry registryEntry, Supplier profiler, boolean isClient, boolean debugWorld, long seed) { 28 | super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed); 29 | } 30 | 31 | @Inject(method = "tick", 32 | at=@At(value = "INVOKE", 33 | target = "Lnet/minecraft/world/EntityList;forEach(Ljava/util/function/Consumer;)V")) 34 | public void preEntities(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 35 | TickProgress.update(ENTITIES, this.getRegistryKey()); 36 | } 37 | 38 | @Inject(method="method_31420(Lnet/minecraft/util/profiler/Profiler;Lnet/minecraft/entity/Entity;)V", 39 | at=@At(value = "INVOKE", 40 | target = "Lnet/minecraft/server/world/ServerWorld;tickEntity(Ljava/util/function/Consumer;Lnet/minecraft/entity/Entity;)V")) 41 | public void preEntity(Profiler profiler, Entity entity, CallbackInfo ci){ 42 | int runStatus = TickProgress.runStatus(); 43 | TickSpeed.process_entities = runStatus == RUN_COMPLETELY || runStatus == STEP_TO_FINISH; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/subtick/commands/HighlightCommand.java: -------------------------------------------------------------------------------- 1 | package subtick.commands; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import carpet.utils.Messenger; 5 | import com.mojang.brigadier.CommandDispatcher; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import subtick.Highlights; 10 | import subtick.progress.TickProgress; 11 | 12 | import static net.minecraft.server.command.CommandManager.argument; 13 | import static net.minecraft.server.command.CommandManager.literal; 14 | 15 | 16 | public class HighlightCommand { 17 | public static void register(CommandDispatcher dispatcher){ 18 | dispatcher.register( 19 | literal("highlight").then( 20 | literal("executed").executes((c) -> commandHighlight(c, Highlights.EXECUTED, 0)) 21 | ).then( 22 | literal("new").executes((c) -> commandHighlight(c, Highlights.NEW_EVENTS, TickProgress.NUM_PHASES)).then( 23 | literal("tt").executes((c)->commandHighlight(c, Highlights.NEW_EVENTS, TickProgress.TILE_TICKS)) 24 | ).then( 25 | literal("be").executes((c)->commandHighlight(c, Highlights.NEW_EVENTS, TickProgress.BLOCK_EVENTS)) 26 | ) 27 | ).then( 28 | literal("none").executes((c)->commandHighlight(c, Highlights.NONE, 0)) 29 | ) 30 | ); 31 | } 32 | 33 | private static int commandHighlight(CommandContext c, int type, int phase){ 34 | if(!TickSpeed.isPaused()){ 35 | Messenger.m(c.getSource(), "wi Must be in tick freeze to highlight"); 36 | return 0; 37 | } 38 | 39 | if(type == Highlights.EXECUTED){ 40 | Messenger.m(c.getSource(), "gi Highlighting Executed Events"); 41 | Highlights.showExecuted(c.getSource().getWorld()); 42 | } else if(type == Highlights.NEW_EVENTS){ 43 | Messenger.m(c.getSource(), "gi Highlighting New Events"); 44 | Highlights.showNew(c.getSource().getWorld(), phase); 45 | } else if(type==Highlights.NONE){ 46 | Messenger.m(c.getSource(), "gi Removing all highlights"); 47 | Highlights.showNone(c.getSource().getWorld()); 48 | } 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/subtick/progress/TickActions.java: -------------------------------------------------------------------------------- 1 | package subtick.progress; 2 | 3 | import net.minecraft.server.command.ServerCommandSource; 4 | import subtick.Highlights; 5 | import subtick.commands.HighlightCommand; 6 | 7 | import java.util.Arrays; 8 | 9 | public class TickActions { 10 | public static final int ANYTHING = -1; 11 | public static final int DEFAULT = 0; 12 | 13 | public static boolean ttSuccess = false;//previous tt was executed on the right block 14 | public static int[] bedCount = new int[3]; 15 | 16 | /*TODO 17 | used for additional granularity within a tick phase (if needed) 18 | block events: 19 | be: 0 20 | bed: 1 21 | tile ticks: 22 | tt: 0 23 | priority: 1 24 | */ 25 | public static int action; 26 | 27 | public static int numActionsStep;//for this tick 28 | 29 | public static int numActionsPlay;//for playing actions 30 | public static int ticksPerStep;//number of ticks between played actions 31 | 32 | public static int tickCount = 0;//tracking real time 33 | 34 | public static ServerCommandSource actor = null; 35 | 36 | 37 | public static void tick(){ 38 | if(numActionsPlay * ticksPerStep >= tickCount){ 39 | if(ticksPerStep == 0){ 40 | numActionsStep = numActionsPlay; 41 | } else if(tickCount % ticksPerStep == 0){ 42 | numActionsStep++; 43 | } 44 | tickCount++; 45 | } 46 | } 47 | 48 | public static void finishPlaying(){ 49 | tickCount = numActionsPlay * ticksPerStep + 1; 50 | } 51 | 52 | public static boolean stillPlaying(){ 53 | return numActionsPlay != 0 && numActionsPlay * ticksPerStep >= tickCount; 54 | } 55 | 56 | public static void play(int numActions, int ticksPerPlay, ServerCommandSource actor){ 57 | play(numActions, ticksPerPlay, actor, 0); 58 | } 59 | 60 | public static void play(int numActions, int ticksPerPlay, ServerCommandSource actor, int action){ 61 | TickActions.ticksPerStep = ticksPerPlay; 62 | TickActions.action = action; 63 | TickActions.actor = actor; 64 | numActionsPlay = numActions; 65 | tickCount = 0; 66 | Highlights.clearHighlights(actor.getServer()); 67 | } 68 | 69 | public static void reset() { 70 | action = 0; 71 | numActionsPlay = 0; 72 | numActionsStep = 0; 73 | tickCount = 0; 74 | actor = null; 75 | ticksPerStep = 0; 76 | Arrays.fill(bedCount, 0); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/main/java/subtick/commands/TTCommand.java: -------------------------------------------------------------------------------- 1 | package subtick.commands; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import carpet.utils.Messenger; 5 | import com.mojang.brigadier.CommandDispatcher; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import net.minecraft.network.MessageType; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import net.minecraft.text.LiteralText; 10 | import subtick.progress.TickActions; 11 | import subtick.progress.TickProgress; 12 | 13 | import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; 14 | import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; 15 | import static net.minecraft.server.command.CommandManager.argument; 16 | import static net.minecraft.server.command.CommandManager.literal; 17 | import static subtick.progress.TickProgress.NUM_PHASES; 18 | 19 | public class TTCommand { 20 | public static void register(CommandDispatcher dispatcher){ 21 | dispatcher.register(literal("tt").then( 22 | literal("step").executes( 23 | (c) -> commandStepTT(c, 1, 0) 24 | ).then( 25 | argument("num", integer(1)).executes( 26 | (c) -> commandStepTT(c, getInteger(c, "num"), 0) 27 | ).then( 28 | argument("ticks", integer(0)).executes( 29 | (c) -> commandStepTT(c, getInteger(c, "num"), getInteger(c, "ticks")) 30 | ) 31 | ) 32 | ) 33 | ).then( 34 | literal("count").executes(TTCommand::commandCountTT) 35 | )); 36 | } 37 | 38 | private static int commandCountTT(CommandContext c){ 39 | int progress = TickProgress.progressOf(tickPhase, c.getSource().getWorld().getRegistryKey()); 40 | if(TickProgress.currentProgress != progress){ 41 | Messenger.m(c.getSource(), "wi Must be stepping " + TickProgress.progressName(progress, true) + " to count"); 42 | return 0;//TODO implement this entire system properly. Actually display what tile ticks are on what ticks 43 | } 44 | Messenger.m(c.getSource(), "gi " + c.getSource().getWorld().getBlockTickScheduler().getTickCount() + 45 | " Tile Ticks remaining"); 46 | return 0; 47 | } 48 | 49 | private static final int tickPhase = TickProgress.TILE_TICKS; 50 | 51 | private static int commandStepTT(CommandContext c, int num, int ticks){ 52 | if(TickSpeed.process_entities){ 53 | Messenger.m(c.getSource(), "wi Game is not frozen"); 54 | return 0; 55 | } 56 | 57 | if(TickActions.stillPlaying()){ 58 | Messenger.m(c.getSource(), "wi Already stepping " + TickProgress.progressName(tickPhase)); 59 | return 0; 60 | } 61 | 62 | int progress = TickProgress.progressOf(tickPhase, c.getSource().getWorld().getRegistryKey()); 63 | if(TickProgress.getDimension(progress) != TickProgress.dim(c.getSource().getWorld().getRegistryKey())){ 64 | Messenger.m(c.getSource(), "wi Must be in same dimension as current tick phase to step."); 65 | return 0; 66 | } 67 | 68 | if(progress < TickProgress.targetProgress){ 69 | Messenger.m(c.getSource(), "wi " + TickProgress.tickPhaseNamesPlural[tickPhase] + " has already passed for this dimension. Use tick step to go to the beginning of the next tick"); 70 | return 0; 71 | } 72 | 73 | TickProgress.setTarget(progress); 74 | TickActions.play(num, ticks, c.getSource()); 75 | return 0; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/WorldTickSchedulerMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins; 2 | 3 | import carpet.utils.Messenger; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.util.profiler.Profiler; 6 | import net.minecraft.world.tick.OrderedTick; 7 | import net.minecraft.world.tick.QueryableTickScheduler; 8 | import net.minecraft.world.tick.WorldTickScheduler; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | import subtick.progress.TickActions; 16 | import subtick.progress.TickProgress; 17 | 18 | import java.util.List; 19 | import java.util.Queue; 20 | import java.util.Set; 21 | import java.util.function.BiConsumer; 22 | import java.util.function.Supplier; 23 | 24 | import static subtick.progress.TickProgress.*; 25 | 26 | @Mixin(value = WorldTickScheduler.class, priority = 1001) 27 | public abstract class WorldTickSchedulerMixin implements QueryableTickScheduler { 28 | @Shadow protected abstract void collectTickableTicks(long time, int maxTicks, Profiler profiler); 29 | 30 | @Shadow @Final private Supplier profilerGetter; 31 | 32 | @Shadow protected abstract void clear(); 33 | 34 | @Shadow protected abstract void tick(BiConsumer ticker); 35 | 36 | @Shadow @Final public Queue> tickableTicks; 37 | 38 | @Shadow @Final private Set> copiedTickableTicksList; 39 | 40 | @Shadow @Final private List> tickedTicks; 41 | 42 | @Inject(method = "tick(JILjava/util/function/BiConsumer;)V", at = @At("HEAD"), cancellable = true) 43 | public void blockTickStep(long time, int maxTicks, BiConsumer ticker, CallbackInfo ci){ 44 | int runStatus = TickProgress.runStatus(); 45 | if(runStatus == RUN_COMPLETELY){ 46 | return; 47 | } 48 | ci.cancel(); 49 | 50 | if(runStatus == STEP_FROM_START){ 51 | collectTickableTicks(time, maxTicks, this.profilerGetter.get()); 52 | } 53 | 54 | if(runStatus == STEP_FROM_START || runStatus == STEP){ 55 | int total = 0; 56 | for (int i = 0; i < TickActions.numActionsStep; i++) { 57 | if(tickableTicks.isEmpty()){ 58 | Messenger.m(TickActions.actor, "gi No more Tile Ticks in this dimension"); 59 | TickActions.tickCount = TickActions.ticksPerStep * TickActions.numActionsPlay + 1; 60 | break; 61 | } 62 | total++; 63 | OrderedTick orderedTick = this.tickableTicks.poll(); 64 | if (!this.copiedTickableTicksList.isEmpty()) { 65 | this.copiedTickableTicksList.remove(orderedTick); 66 | } 67 | 68 | this.tickedTicks.add(orderedTick); 69 | if(!TickActions.ttSuccess){ 70 | i--; 71 | total--; 72 | } 73 | ticker.accept(orderedTick.pos(), orderedTick.type()); 74 | } 75 | 76 | if(TickActions.numActionsStep != 0 && !TickActions.stillPlaying()){ 77 | int totalStepped; 78 | if(tickableTicks.isEmpty()){ 79 | if(TickActions.ticksPerStep == 0){ 80 | totalStepped = total; 81 | } else { 82 | totalStepped = (TickActions.tickCount-1)/TickActions.ticksPerStep + total; 83 | } 84 | } else { 85 | totalStepped = TickActions.numActionsPlay; 86 | } 87 | if(totalStepped != 0) 88 | Messenger.m(TickActions.actor, "wi Stepped " + totalStepped + " tile tick" + (totalStepped==1?"":"s")); 89 | } 90 | } 91 | 92 | if(runStatus == STEP_TO_FINISH){ 93 | tick(ticker); 94 | clear(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/world_tickphases/TileTicksWorldMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.world_tickphases; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import net.minecraft.block.Block; 5 | import net.minecraft.fluid.Fluid; 6 | import net.minecraft.server.world.ServerWorld; 7 | import net.minecraft.util.math.BlockPos; 8 | import net.minecraft.util.profiler.Profiler; 9 | import net.minecraft.util.registry.RegistryEntry; 10 | import net.minecraft.util.registry.RegistryKey; 11 | import net.minecraft.world.MutableWorldProperties; 12 | import net.minecraft.world.StructureWorldAccess; 13 | import net.minecraft.world.World; 14 | import net.minecraft.world.dimension.DimensionType; 15 | import net.minecraft.world.tick.WorldTickScheduler; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.Redirect; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | import subtick.SubTickSettings; 22 | import subtick.progress.TickActions; 23 | import subtick.progress.TickProgress; 24 | 25 | import java.util.function.BiConsumer; 26 | import java.util.function.BooleanSupplier; 27 | import java.util.function.Supplier; 28 | 29 | import static subtick.progress.TickProgress.*; 30 | 31 | @Mixin(value = ServerWorld.class, priority = 999) 32 | public abstract class TileTicksWorldMixin extends World implements StructureWorldAccess { 33 | 34 | protected TileTicksWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry registryEntry, Supplier profiler, boolean isClient, boolean debugWorld, long seed) { 35 | super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed); 36 | } 37 | 38 | 39 | /* 40 | world border, weather, time 41 | */ 42 | @Inject(method="tick", at=@At("HEAD")) 43 | public void tickStart(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 44 | int runStatus = TickProgress.update(TILE_TICKS, this.getRegistryKey()); 45 | if(runStatus == RUN_COMPLETELY || runStatus == STEP_FROM_START){ 46 | TickSpeed.process_entities = true; 47 | } 48 | } 49 | 50 | @Inject(method = "tick", at=@At(value = "INVOKE", 51 | target = "Lnet/minecraft/server/world/ServerWorld;isDebugWorld()Z")) 52 | public void preDebug(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 53 | TickSpeed.process_entities = true; 54 | } 55 | 56 | @Redirect(method = "tick", at=@At(value = "INVOKE", 57 | target = "Lnet/minecraft/world/tick/WorldTickScheduler;tick(JILjava/util/function/BiConsumer;)V", 58 | ordinal = 0)) 59 | public void onTileTicks(WorldTickScheduler instance, long time, int maxTicks, BiConsumer ticker){ 60 | int runStatus = TickProgress.runStatus(); 61 | if(runStatus == NO_RUN){ 62 | return; 63 | } 64 | instance.tick(time, maxTicks, ticker);//tickscheduler mixins will handle run types, just need to know they are the ones currently being run 65 | } 66 | 67 | @Redirect(method = "tick", at=@At(value = "INVOKE", 68 | target = "Lnet/minecraft/world/tick/WorldTickScheduler;tick(JILjava/util/function/BiConsumer;)V", 69 | ordinal = 1)) 70 | public void onLiquidTicks(WorldTickScheduler instance, long time, int maxTicks, BiConsumer ticker){ 71 | int runStatus = TickProgress.update(LIQUID_TICKS, this.getRegistryKey()); 72 | if(runStatus == NO_RUN){ 73 | return; 74 | } 75 | instance.tick(time, maxTicks, ticker);//tickscheduler mixins will handle run types, just need to know they are the ones currently being run 76 | TickSpeed.process_entities = runStatus==RUN_COMPLETELY || runStatus==STEP_TO_FINISH; 77 | } 78 | 79 | @Inject(method="tickBlock", at=@At("HEAD")) 80 | public void verifyBlock(BlockPos pos, Block block, CallbackInfo ci){ 81 | TickActions.ttSuccess = !SubTickSettings.skipInvalidEvents || this.getBlockState(pos).isOf(block); 82 | } 83 | 84 | @Inject(method="tickFluid", at=@At("HEAD")) 85 | public void verifyFluid(BlockPos pos, Fluid fluid, CallbackInfo ci){ 86 | TickActions.ttSuccess = !SubTickSettings.skipInvalidEvents || this.getFluidState(pos).isOf(fluid); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/subtick/commands/BECommand.java: -------------------------------------------------------------------------------- 1 | package subtick.commands; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import carpet.utils.Messenger; 5 | import com.mojang.brigadier.CommandDispatcher; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import subtick.progress.TickActions; 10 | import subtick.progress.TickProgress; 11 | 12 | import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; 13 | import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; 14 | import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; 15 | import static net.minecraft.server.command.CommandManager.argument; 16 | import static subtick.progress.TickProgress.NUM_PHASES; 17 | 18 | public class BECommand { 19 | public static void register(CommandDispatcher dispatcher){ 20 | dispatcher.register(CommandManager.literal("be").then( 21 | CommandManager.literal("step").executes( 22 | (c) -> commandStepBE(c, 1, 0, 0) 23 | ).then( 24 | argument("num", integer(1)).executes( 25 | (c) -> commandStepBE(c, getInteger(c, "num"), 0, 0) 26 | ).then( 27 | argument("ticks", integer(0)).executes( 28 | (c) -> commandStepBE(c, getInteger(c, "num"), getInteger(c, "ticks"), 0) 29 | ) 30 | ) 31 | ) 32 | ).then( 33 | CommandManager.literal("count").executes((c) -> commandCountBE(c, 0)) 34 | )); 35 | 36 | dispatcher.register(CommandManager.literal("bed").then( 37 | CommandManager.literal("step").executes( 38 | (c) -> commandStepBE(c, 1, 0, 1) 39 | ).then( 40 | argument("num", integer(1)).executes( 41 | (c) -> commandStepBE(c, getInteger(c, "num"), 0, 1) 42 | ).then( 43 | argument("ticks", integer(0)).executes( 44 | (c) -> commandStepBE(c, getInteger(c, "num"), getInteger(c, "ticks"), 1) 45 | ) 46 | ) 47 | ) 48 | ).then( 49 | CommandManager.literal("count").executes((c) -> commandCountBE(c, 1)) 50 | )); 51 | 52 | } 53 | 54 | private static int commandCountBE(CommandContext c, int action){ 55 | if(action==0){ 56 | Messenger.m(c.getSource(), "wi " + c.getSource().getWorld().syncedBlockEventQueue.size() + " Block Events remaining"); 57 | } else { 58 | int bedCount; 59 | if(TickProgress.currentProgress == TickProgress.progressOf(tickPhase, c.getSource().getWorld().getRegistryKey())){ 60 | bedCount = TickActions.bedCount[TickProgress.dim(c.getSource().getWorld().getRegistryKey())/NUM_PHASES]; 61 | } else { 62 | bedCount = c.getSource().getWorld().syncedBlockEventQueue.size(); 63 | } 64 | Messenger.m(c.getSource(), "wi " + bedCount 65 | + " Block Events remaining at the current depth"); 66 | } 67 | return 0; 68 | } 69 | 70 | private static final int tickPhase = TickProgress.BLOCK_EVENTS; 71 | 72 | private static int commandStepBE(CommandContext c, int num, int ticks, int action){ 73 | if(TickSpeed.process_entities){ 74 | Messenger.m(c.getSource(), "wi Game is not frozen"); 75 | return 0; 76 | } 77 | 78 | if(TickActions.stillPlaying()){ 79 | Messenger.m(c.getSource(), "wi Already stepping " + TickProgress.progressName(tickPhase)); 80 | return 0; 81 | } 82 | 83 | int progress = TickProgress.progressOf(tickPhase, c.getSource().getWorld().getRegistryKey()); 84 | if(TickProgress.getDimension(progress) != TickProgress.dim(c.getSource().getWorld().getRegistryKey())){ 85 | Messenger.m(c.getSource(), "wi Must be in same dimension as current tick phase to step.");//trust me, its better this way 86 | return 0; 87 | } 88 | 89 | if(progress < TickProgress.targetProgress){ 90 | Messenger.m(c.getSource(), "wi " + TickProgress.tickPhaseNamesPlural[tickPhase] + " has already passed for this dimension. Use tick step to go to the beginning of the next tick"); 91 | return 0; 92 | } 93 | 94 | TickProgress.setTarget(progress); 95 | TickActions.play(num, ticks, c.getSource(), action); 96 | return 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/subtick/progress/TickProgress.java: -------------------------------------------------------------------------------- 1 | package subtick.progress; 2 | 3 | import net.minecraft.server.world.ServerWorld; 4 | import net.minecraft.util.registry.RegistryKey; 5 | import net.minecraft.world.World; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /*TODO brief list of plans 11 | * subtick step 12 | * subtick step tt/lt/rd/be/bed/en/te 13 | * subtick play 14 | * subtick play tt/be/bed/en/te 15 | * subtick log tt/lt/be/en/te 16 | * subtick log clear 17 | * subtick when/list 18 | 19 | */ 20 | 21 | public class TickProgress { 22 | 23 | public static final boolean DEBUG = false; 24 | 25 | public static final int TILE_TICKS = 0, 26 | LIQUID_TICKS = 1, 27 | RAIDS = 2, 28 | BLOCK_EVENTS = 3, 29 | ENTITIES = 4, 30 | TILE_ENTITIES = 5, 31 | NUM_PHASES = 6; 32 | 33 | public static final int 34 | OVERWORLD = 0, //0 35 | END = NUM_PHASES, //6 36 | NETHER = NUM_PHASES*2, //12 37 | NONE = -1; 38 | 39 | public static final int POST_TICK = NUM_PHASES*3; 40 | public static final int PRE_TICK = -1; 41 | 42 | public static int currentProgress = PRE_TICK, targetProgress = POST_TICK; 43 | 44 | private static final Map, Integer> dimensionCodes; 45 | private static final RegistryKey[] dimensions; 46 | 47 | public static final String[] dimensionNames = new String[]{"Overworld", "End", "Nether",}; 48 | public static final String[] tickPhaseNamesPlural = new String[]{"Tile Ticks", "Liquid Ticks", "Raids", "Block Events", "Entities", "Tile Entities"}; 49 | public static final String[] tickPhaseNames = new String[]{"Tile Tick", "Liquid Tick", "Raid", "Block Event", "Entity", "Tile Entity"}; 50 | 51 | static { 52 | dimensionCodes = new HashMap<>(); 53 | dimensionCodes.put(ServerWorld.OVERWORLD, OVERWORLD); 54 | dimensionCodes.put(ServerWorld.END, END); 55 | dimensionCodes.put(ServerWorld.NETHER, NETHER); 56 | dimensions = new RegistryKey[]{ 57 | ServerWorld.OVERWORLD, 58 | ServerWorld.END, 59 | ServerWorld.NETHER 60 | }; 61 | } 62 | 63 | public static int getDimension(int progress){ 64 | return (progress== PRE_TICK || progress== POST_TICK) ? NONE : ((progress/NUM_PHASES)*NUM_PHASES); 65 | } 66 | 67 | public static int getTickPhase(int progress){ 68 | return progress== PRE_TICK || progress== POST_TICK ? NONE : (progress%NUM_PHASES); 69 | } 70 | 71 | public static int progressOf(int tickPhase, int dimension){ 72 | return dimension+tickPhase; 73 | } 74 | 75 | public static int progressOf(int tickPhase, RegistryKey dimension){ 76 | return progressOf(tickPhase, dim(dimension)); 77 | } 78 | 79 | public static int dim(RegistryKey dimension){ 80 | return dimensionCodes.get(dimension); 81 | } 82 | 83 | public static RegistryKey dim(int dimension){ 84 | return dimensions[dimension]; 85 | } 86 | 87 | public static String progressName(int progress){ 88 | return progressName(progress, true); 89 | } 90 | 91 | public static String progressName(int progress, boolean plural){ 92 | return progress== PRE_TICK ?"Pre Tick": 93 | progress== POST_TICK ?"Post Tick": 94 | ((plural ? tickPhaseNamesPlural:tickPhaseNames)[getTickPhase(progress)] + " in the " + dimensionNames[getDimension(progress)/NUM_PHASES]); 95 | } 96 | 97 | public static void setTarget(int phase, int dimension){ 98 | targetProgress = progressOf(phase, dimension); 99 | } 100 | 101 | public static void setTarget(int progress){ 102 | targetProgress = progress; 103 | } 104 | 105 | public static void setCurrent(int progress){ 106 | currentProgress = progress; 107 | } 108 | 109 | public static void reset(){ 110 | targetProgress = POST_TICK; 111 | currentProgress = PRE_TICK; 112 | runStatus = RUN_COMPLETELY; 113 | } 114 | 115 | /* 116 | RUN_COMPLETELY: call the default minecraft code for the tick phase (ensures lithium is left untouched) 117 | STEP_TO_FINISH: run subtick code to the end of the tick phase 118 | STEP_FROM_START: run subtick code for a tick phase, including the beginning part 119 | STEP: run only the stepping event code 120 | NO_RUN: skip this tick phase 121 | */ 122 | 123 | public static final int RUN_COMPLETELY = 0, 124 | STEP_TO_FINISH = 1, 125 | STEP_FROM_START = 2, 126 | STEP = 3, 127 | NO_RUN = 4; 128 | 129 | private static int runStatus = 0; 130 | 131 | public static int update(int phase, RegistryKey dimension){ 132 | int ret = update(phase, dim(dimension)); 133 | if(DEBUG) { 134 | System.out.println(overallStatus() + ", immediate: \"" + progressName(progressOf(phase, dim(dimension)))); 135 | } 136 | return ret; 137 | } 138 | 139 | public static int update(int phase, int dimension){ 140 | int progress = progressOf(phase, dimension); 141 | return update(progress); 142 | } 143 | 144 | public static int update(int progress){ 145 | if(progress < currentProgress || progress > targetProgress){ 146 | runStatus = NO_RUN; 147 | return runStatus; 148 | } 149 | 150 | if(progress == currentProgress){ 151 | if(currentProgress < targetProgress){ 152 | runStatus = STEP_TO_FINISH; 153 | } else { 154 | runStatus = STEP; 155 | } 156 | return runStatus; 157 | } 158 | 159 | currentProgress = progress; 160 | if(progress < targetProgress){ 161 | runStatus = RUN_COMPLETELY; 162 | } else { 163 | runStatus = STEP_FROM_START; 164 | } 165 | return runStatus; 166 | } 167 | 168 | public static int runStatus() { 169 | return runStatus; 170 | } 171 | 172 | public static String overallStatus(){ 173 | return "Target: \"" + progressName(targetProgress) + "\", Current: \"" + progressName(currentProgress) + "\", run status: " + runStatus; 174 | } 175 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /src/main/java/subtick/Highlights.java: -------------------------------------------------------------------------------- 1 | package subtick; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import it.unimi.dsi.fastutil.ints.IntList; 5 | import net.minecraft.block.Block; 6 | import net.minecraft.block.Blocks; 7 | import net.minecraft.entity.FallingBlockEntity; 8 | import net.minecraft.network.Packet; 9 | import net.minecraft.network.packet.s2c.play.EntitiesDestroyS2CPacket; 10 | import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; 11 | import net.minecraft.network.packet.s2c.play.TeamS2CPacket; 12 | import net.minecraft.scoreboard.Scoreboard; 13 | import net.minecraft.scoreboard.Team; 14 | import net.minecraft.server.MinecraftServer; 15 | import net.minecraft.server.network.ServerPlayerEntity; 16 | import net.minecraft.server.world.ServerWorld; 17 | import net.minecraft.text.LiteralText; 18 | import net.minecraft.text.Style; 19 | import net.minecraft.util.Formatting; 20 | import net.minecraft.util.math.Vec3i; 21 | import subtick.progress.TickProgress; 22 | 23 | import java.util.*; 24 | 25 | /*What will highlights be used for: 26 | * Showing where events happened (white) 27 | * Showing the order events happened if multiple 28 | * Showing newly scheduled events (green) 29 | * Showing currently scheduled events (blue) 30 | */ 31 | public class Highlights { 32 | 33 | private static final Set executed = new HashSet<>(); 34 | private static final Set[] newHighlights = new HashSet[TickProgress.NUM_PHASES]; 35 | 36 | private static final Scoreboard dummyScoreboard = new Scoreboard(); 37 | private static final Team[] teams; 38 | 39 | public static final int EXECUTED = 0, SCHEDULED = 1, NEW_EVENTS = 2, NONE = -1; 40 | 41 | public static int type; 42 | public static Map currentShowed = new HashMap<>(); 43 | 44 | private static final Block[] highlightStates = new Block[]{ 45 | Blocks.WHITE_STAINED_GLASS, 46 | Blocks.BLUE_STAINED_GLASS, 47 | Blocks.GREEN_STAINED_GLASS 48 | }; 49 | 50 | static { 51 | teams = new Team[]{ 52 | new Team(dummyScoreboard, "executed"), 53 | new Team(dummyScoreboard, "scheduled"), 54 | new Team(dummyScoreboard, "new") 55 | }; 56 | teams[0].setColor(Formatting.WHITE); 57 | teams[1].setColor(Formatting.BLUE); 58 | teams[2].setColor(Formatting.GREEN); 59 | } 60 | 61 | public static void executedHighlight(Vec3i pos, ServerWorld world){ 62 | executed.add(pos); 63 | if(type==EXECUTED && TickSpeed.isPaused()) 64 | showHighlight(pos, EXECUTED, "", world); 65 | } 66 | 67 | public static void newHighlight(Vec3i pos, int tickPhase, ServerWorld world){ 68 | newHighlights[tickPhase].add(pos); 69 | if(type==NEW_EVENTS && TickSpeed.isPaused()) 70 | showHighlight(pos, NEW_EVENTS, "", world); 71 | } 72 | 73 | public static void showExecuted(ServerWorld world){ 74 | hideHighlights(world.getServer()); 75 | Highlights.type = EXECUTED; 76 | world.getServer().getPlayerManager().sendToAll(TeamS2CPacket.updateTeam(teams[type], true)); 77 | 78 | for(Vec3i pos : executed){ 79 | showHighlight(pos, EXECUTED, "", world); 80 | } 81 | } 82 | 83 | public static void showNew(ServerWorld world, int phase){ 84 | hideHighlights(world.getServer()); 85 | Highlights.type = NEW_EVENTS; 86 | world.getServer().getPlayerManager().sendToAll(TeamS2CPacket.updateTeam(teams[type], true)); 87 | if(phase==TickProgress.NUM_PHASES){ 88 | for (int j = 0; j < TickProgress.NUM_PHASES; j++) { 89 | Set highlights = newHighlights[j]; 90 | for(Vec3i pos : highlights){ 91 | showHighlight(pos, type, "", world); 92 | } 93 | } 94 | } else { 95 | for(Vec3i pos : newHighlights[phase]){ 96 | showHighlight(pos, type, "", world); 97 | } 98 | } 99 | } 100 | 101 | public static void showScheduled(MinecraftServer server, int phase){ 102 | hideHighlights(server); 103 | Highlights.type = SCHEDULED; 104 | server.getPlayerManager().sendToAll(TeamS2CPacket.updateTeam(teams[type], true)); 105 | } 106 | 107 | //to clear highlights when stepping or unfreezing 108 | public static void clearHighlights(MinecraftServer server){ 109 | hideHighlights(server); 110 | executed.clear(); 111 | for(Set list : newHighlights){ 112 | list.clear(); 113 | } 114 | } 115 | 116 | public static void showNone(ServerWorld world) { 117 | hideHighlights(world.getServer()); 118 | Highlights.type = NONE; 119 | } 120 | 121 | private static void hideHighlights(MinecraftServer server){ 122 | if(type==NONE){ 123 | return; 124 | } 125 | Team team = teams[type]; 126 | for(String name : team.getPlayerList()){ 127 | server.getPlayerManager().sendToAll(TeamS2CPacket.changePlayerTeam(team, name, TeamS2CPacket.Operation.REMOVE)); 128 | } 129 | EntitiesDestroyS2CPacket destroyPacket = new EntitiesDestroyS2CPacket(); 130 | IntList ids = destroyPacket.getEntityIds(); 131 | for(FallingBlockEntity highlight : currentShowed.values()){ 132 | ids.add(highlight.getId()); 133 | } 134 | currentShowed.clear(); 135 | server.getPlayerManager().sendToAll(destroyPacket); 136 | } 137 | 138 | private static void hideHighlight(Vec3i pos, MinecraftServer server){ 139 | if(type==NONE){ 140 | return; 141 | } 142 | if(!currentShowed.containsKey(pos)){ 143 | return; 144 | } 145 | 146 | FallingBlockEntity highlight = currentShowed.get(pos); 147 | Team team = teams[type]; 148 | EntitiesDestroyS2CPacket destroyPacket = new EntitiesDestroyS2CPacket(); 149 | IntList ids = destroyPacket.getEntityIds(); 150 | ids.add(highlight.getId()); 151 | currentShowed.remove(pos); 152 | 153 | server.getPlayerManager().sendToAll(TeamS2CPacket.changePlayerTeam(team, highlight.getEntityName(), TeamS2CPacket.Operation.REMOVE)); 154 | server.getPlayerManager().sendToAll(destroyPacket); 155 | } 156 | 157 | private static void showHighlight(Vec3i pos, int type, String name, ServerWorld world){ 158 | if(currentShowed.containsKey(pos)){ 159 | return; 160 | } 161 | Block block = highlightStates[type]; 162 | FallingBlockEntity highlight = new FallingBlockEntity(world, (double)pos.getX()+0.5, pos.getY()-1/48d, (double)pos.getZ()+.5, block.getDefaultState()); 163 | highlight.setNoGravity(true); 164 | highlight.setGlowing(true); 165 | highlight.setInvulnerable(true); 166 | highlight.setCustomNameVisible(true); 167 | if(name != null && !name.isEmpty()) { 168 | highlight.setCustomName(new LiteralText(name).setStyle(Style.EMPTY.withColor(-1).withBold(true))); 169 | } 170 | 171 | Packet spawnPacket = highlight.createSpawnPacket(); 172 | EntityTrackerUpdateS2CPacket dataPacket = new EntityTrackerUpdateS2CPacket(highlight.getId(), highlight.getDataTracker(), true); 173 | List players = world.getPlayers(); 174 | 175 | for(ServerPlayerEntity player : players){ 176 | player.networkHandler.sendPacket(spawnPacket); 177 | player.networkHandler.sendPacket(dataPacket); 178 | } 179 | 180 | teams[type].getPlayerList().add(highlight.getEntityName()); 181 | world.getServer().getPlayerManager().sendToAll(TeamS2CPacket.changePlayerTeam(teams[type], highlight.getEntityName(), TeamS2CPacket.Operation.ADD)); 182 | currentShowed.put(pos, highlight); 183 | } 184 | 185 | public static void reset() { 186 | type = 0; 187 | executed.clear(); 188 | for (int i = 0; i < TickProgress.NUM_PHASES; i++) { 189 | newHighlights[i].clear(); 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /src/main/java/subtick/mixins/world_tickphases/BlockEventWorldMixin.java: -------------------------------------------------------------------------------- 1 | package subtick.mixins.world_tickphases; 2 | 3 | import carpet.helpers.TickSpeed; 4 | import carpet.utils.Messenger; 5 | import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; 6 | import net.minecraft.network.packet.s2c.play.BlockEventS2CPacket; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraft.server.world.BlockEvent; 9 | import net.minecraft.server.world.ServerWorld; 10 | import net.minecraft.util.math.ChunkPos; 11 | import net.minecraft.util.profiler.Profiler; 12 | import net.minecraft.util.registry.RegistryEntry; 13 | import net.minecraft.util.registry.RegistryKey; 14 | import net.minecraft.world.MutableWorldProperties; 15 | import net.minecraft.world.StructureWorldAccess; 16 | import net.minecraft.world.World; 17 | import net.minecraft.world.dimension.DimensionType; 18 | import org.spongepowered.asm.mixin.Final; 19 | import org.spongepowered.asm.mixin.Mixin; 20 | import org.spongepowered.asm.mixin.Shadow; 21 | import org.spongepowered.asm.mixin.injection.At; 22 | import org.spongepowered.asm.mixin.injection.Inject; 23 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 24 | import subtick.Highlights; 25 | import subtick.SubTickSettings; 26 | import subtick.progress.TickActions; 27 | import subtick.progress.TickProgress; 28 | 29 | import java.util.List; 30 | import java.util.function.BooleanSupplier; 31 | import java.util.function.Supplier; 32 | 33 | import static subtick.progress.TickProgress.*; 34 | 35 | @Mixin(value = ServerWorld.class, priority = 999) 36 | public abstract class BlockEventWorldMixin extends World implements StructureWorldAccess { 37 | @Shadow @Final public ObjectLinkedOpenHashSet syncedBlockEventQueue; 38 | 39 | @Shadow @Final private List blockEventQueue; 40 | 41 | @Shadow public abstract boolean shouldTickBlocksInChunk(long chunkPos); 42 | 43 | @Shadow protected abstract boolean processBlockEvent(BlockEvent event); 44 | 45 | @Shadow @Final private MinecraftServer server; 46 | 47 | protected BlockEventWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry registryEntry, Supplier profiler, boolean isClient, boolean debugWorld, long seed) { 48 | super(properties, registryRef, registryEntry, profiler, isClient, debugWorld, seed); 49 | } 50 | 51 | @Inject(method="tick", at=@At(value = "INVOKE", target = "Lnet/minecraft/server/world/ServerWorld;processSyncedBlockEvents()V")) 52 | public void preBlockEvents(BooleanSupplier shouldKeepTicking, CallbackInfo ci){ 53 | int runStatus = TickProgress.update(BLOCK_EVENTS, this.getRegistryKey()); 54 | TickSpeed.process_entities = runStatus != NO_RUN; 55 | } 56 | 57 | private int bedCount = 0; 58 | 59 | @Inject(method = "processSyncedBlockEvents", at=@At("HEAD"), cancellable = true) 60 | public void onProcessBlockEvents(CallbackInfo ci){ 61 | int runStatus = TickProgress.runStatus(); 62 | if(runStatus == RUN_COMPLETELY){ 63 | return; 64 | } 65 | ci.cancel(); 66 | 67 | if(runStatus == STEP_FROM_START){ 68 | this.blockEventQueue.clear(); 69 | bedCount = syncedBlockEventQueue.size(); 70 | } 71 | 72 | if(runStatus == STEP_FROM_START || runStatus == STEP){ 73 | int total; 74 | if(TickActions.action==0) { 75 | total = stepBlockEvents(); 76 | } else { 77 | total = stepBlockEventDelay(); 78 | } 79 | 80 | if(TickActions.numActionsStep != 0 && !TickActions.stillPlaying()){ 81 | int totalStepped; 82 | if(syncedBlockEventQueue.isEmpty()){ 83 | if(TickActions.ticksPerStep == 0){ 84 | totalStepped = total; 85 | } else { 86 | totalStepped = (TickActions.tickCount-1)/TickActions.ticksPerStep + total; 87 | } 88 | } else { 89 | totalStepped = TickActions.numActionsPlay; 90 | } 91 | if(totalStepped != 0) 92 | Messenger.m(TickActions.actor, "gi Stepped " + totalStepped + " block event" + (TickActions.action==1 ?" delay" : (totalStepped==1?"":"s"))); 93 | } 94 | TickActions.bedCount[TickProgress.dim(getRegistryKey())/NUM_PHASES] = bedCount; 95 | } 96 | 97 | if(runStatus == STEP_TO_FINISH){ 98 | while(!this.syncedBlockEventQueue.isEmpty()) { 99 | BlockEvent blockEvent = this.syncedBlockEventQueue.removeFirst(); 100 | if (this.shouldTickBlocksInChunk(ChunkPos.toLong(blockEvent.pos()))) { 101 | if (this.processBlockEvent(blockEvent)) { 102 | sendBlockEvent(blockEvent); 103 | } 104 | } else { 105 | this.blockEventQueue.add(blockEvent); 106 | } 107 | } 108 | syncedBlockEventQueue.addAll(blockEventQueue); 109 | } 110 | } 111 | 112 | private int stepBlockEventDelay() { 113 | int total = 0; 114 | for (int i = 0; i < TickActions.numActionsStep; i++) { 115 | if(syncedBlockEventQueue.isEmpty()){ 116 | Messenger.m(TickActions.actor, "gi No more Block Events in this dimension"); 117 | TickActions.tickCount = TickActions.ticksPerStep * TickActions.numActionsPlay + 1; 118 | return total; 119 | } 120 | total++; 121 | int bedCount = this.bedCount; 122 | for (int j = 0; j < bedCount; j++) { 123 | if(syncedBlockEventQueue.isEmpty()){//just in case, shouldn't happen under 'normal' circumstances 124 | Messenger.m(TickActions.actor, "gi No more Block Events in this dimension"); 125 | TickActions.tickCount = TickActions.ticksPerStep * TickActions.numActionsPlay + 1; 126 | TickActions.action = 0; 127 | return total; 128 | } 129 | BlockEvent blockEvent = this.syncedBlockEventQueue.removeFirst(); 130 | 131 | if(this.shouldTickBlocksInChunk(ChunkPos.toLong(blockEvent.pos()))){ 132 | if (this.processBlockEvent(blockEvent)) { 133 | sendBlockEvent(blockEvent); 134 | } 135 | } else { 136 | this.blockEventQueue.add(blockEvent); 137 | } 138 | } 139 | this.bedCount = syncedBlockEventQueue.size(); 140 | } 141 | return total; 142 | } 143 | 144 | private int stepBlockEvents(){ 145 | int total = 0; 146 | for (int i = 0; i < TickActions.numActionsStep; i++) { 147 | if(syncedBlockEventQueue.isEmpty()){ 148 | Messenger.m(TickActions.actor, "gi No more Block Events in this dimension"); 149 | TickActions.tickCount = TickActions.ticksPerStep * TickActions.numActionsPlay + 1; 150 | return total; 151 | } 152 | BlockEvent blockEvent = this.syncedBlockEventQueue.removeFirst(); 153 | bedCount--; 154 | 155 | if(this.shouldTickBlocksInChunk(ChunkPos.toLong(blockEvent.pos()))){ 156 | if(!SubTickSettings.skipInvalidEvents || this.getBlockState(blockEvent.pos()).isOf(blockEvent.block())){ 157 | total++; 158 | Highlights.executedHighlight(blockEvent.pos(), (ServerWorld)(Object)this); 159 | if (this.processBlockEvent(blockEvent)) { 160 | sendBlockEvent(blockEvent); 161 | } 162 | } 163 | } else { 164 | this.blockEventQueue.add(blockEvent); 165 | i--; 166 | } 167 | 168 | if(bedCount==0){ 169 | bedCount = syncedBlockEventQueue.size(); 170 | } 171 | } 172 | return total; 173 | } 174 | 175 | private void sendBlockEvent(BlockEvent blockEvent){ 176 | this.server 177 | .getPlayerManager() 178 | .sendToAround( 179 | null, 180 | (double)blockEvent.pos().getX(), 181 | (double)blockEvent.pos().getY(), 182 | (double)blockEvent.pos().getZ(), 183 | 64.0, 184 | this.getRegistryKey(), 185 | new BlockEventS2CPacket(blockEvent.pos(), blockEvent.block(), blockEvent.type(), blockEvent.data()) 186 | ); 187 | } 188 | } 189 | --------------------------------------------------------------------------------