Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

schedule bonus ticks #2253

Open
wants to merge 4 commits into
base: mc1.18.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/main/java/moze_intel/projecte/PECore.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import moze_intel.projecte.gameObjs.customRecipes.TomeEnabledCondition;
import moze_intel.projecte.gameObjs.items.ItemPE;
import moze_intel.projecte.gameObjs.items.rings.Arcana;
import moze_intel.projecte.gameObjs.items.rings.TimeWatch;
import moze_intel.projecte.gameObjs.registries.PEBlockEntityTypes;
import moze_intel.projecte.gameObjs.registries.PEBlocks;
import moze_intel.projecte.gameObjs.registries.PEContainerTypes;
Expand Down Expand Up @@ -94,10 +95,7 @@
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
import net.minecraftforge.common.crafting.CraftingHelper;
import net.minecraftforge.event.AddReloadListenerEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.event.TagsUpdatedEvent;
import net.minecraftforge.event.*;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.event.server.ServerStoppedEvent;
import net.minecraftforge.eventbus.api.IEventBus;
Expand Down Expand Up @@ -169,12 +167,27 @@ public PECore() {
MinecraftForge.EVENT_BUS.addListener(this::tagsUpdated);
MinecraftForge.EVENT_BUS.addListener(this::registerCommands);
MinecraftForge.EVENT_BUS.addListener(this::serverStarting);
MinecraftForge.EVENT_BUS.addListener(this::onTickEnd);
MinecraftForge.EVENT_BUS.addListener(this::onClientTickEnd);
MinecraftForge.EVENT_BUS.addListener(this::serverQuit);

//Register our config files
ProjectEConfig.register();
}

private void onTickEnd(TickEvent.WorldTickEvent worldTick) {
if (worldTick.phase == TickEvent.Phase.END) {
Level world = worldTick.world;
TimeWatch.completeTick(world, worldTick::haveTime);
}
}

private void onClientTickEnd(TickEvent.ClientTickEvent clientTick) {
if (clientTick.phase == TickEvent.Phase.END) {
TimeWatch.completeTick(null, () -> true);
}
}

private void registerRecipeSerializers(RegistryEvent.Register<RecipeSerializer<?>> event) {
//Add our condition serializers
CraftingHelper.register(TomeEnabledCondition.SERIALIZER);
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/moze_intel/projecte/config/ServerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public static class Effects {

public final CachedIntValue timePedBonus;
public final CachedDoubleValue timePedMobSlowness;
public final CachedDoubleValue maxTPSLoss;
public final CachedBooleanValue interdictionMode;

private Effects(IPEConfig config, ForgeConfigSpec.Builder builder) {
Expand All @@ -118,6 +119,9 @@ private Effects(IPEConfig config, ForgeConfigSpec.Builder builder) {
timePedMobSlowness = CachedDoubleValue.wrap(config, builder
.comment("Factor the Watch of Flowing Time slows down mobs by while in the pedestal. Set to 1.0 for no slowdown.")
.defineInRange("timePedMobSlowness", 0.10, 0, 1));
maxTPSLoss = CachedDoubleValue.wrap(config, builder
.comment("Maximum TPS loss allowed before the Watch of Flowing Time will stop working. Set to -1.0 to disable.")
.defineInRange("maxTPSLoss", 5.0, -1, 20));
interdictionMode = CachedBooleanValue.wrap(config, builder
.comment("If true the Interdiction Torch only affects hostile mobs and projectiles. If false it affects all non blacklisted living entities.")
.define("interdictionMode", true));
Expand Down Expand Up @@ -154,6 +158,7 @@ private Cooldown(IPEConfig config, ForgeConfigSpec.Builder builder) {
.push("cooldown");
pedestal = new Pedestal(config, builder);
player = new Player(config, builder);

builder.pop();
}

Expand Down
237 changes: 200 additions & 37 deletions src/main/java/moze_intel/projecte/gameObjs/items/rings/TimeWatch.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package moze_intel.projecte.gameObjs.items.rings;

import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.function.BooleanSupplier;

import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import moze_intel.projecte.api.block_entity.IDMPedestal;
import moze_intel.projecte.api.capabilities.item.IItemCharge;
import moze_intel.projecte.api.capabilities.item.IPedestalItem;
Expand Down Expand Up @@ -50,6 +52,9 @@

public class TimeWatch extends PEToggleItem implements IPedestalItem, IItemCharge, IBarHelper {

public static final Map<Level, Long2ObjectOpenHashMap<SpedUpBlockEntity>> queuedBlockEntityTicks = new WeakHashMap<>();
public static final Map<Level, Long2ObjectOpenHashMap<SpedUpRandomBlock>> queuedRandomTicks = new WeakHashMap<>();

public TimeWatch(Properties props) {
super(props);
addItemCapability(PedestalItemCapabilityWrapper::new);
Expand Down Expand Up @@ -133,33 +138,17 @@ private void speedUpBlockEntities(Level level, int bonusTicks, AABB bBox) {
// Sanity check the box for chunk unload weirdness
return;
}
Long2ObjectOpenHashMap<SpedUpBlockEntity> levelBlockEntityTicks = queuedBlockEntityTicks.computeIfAbsent(level, k -> new Long2ObjectOpenHashMap<>());
for (BlockEntity blockEntity : WorldHelper.getBlockEntitiesWithinAABB(level, bBox)) {
if (!blockEntity.isRemoved() && !BlockEntities.BLACKLIST_TIME_WATCH_LOOKUP.contains(blockEntity.getType())) {
BlockPos pos = blockEntity.getBlockPos();
if (level.shouldTickBlocksAt(ChunkPos.asLong(pos))) {
LevelChunk chunk = level.getChunkAt(pos);
RebindableTickingBlockEntityWrapper tickingWrapper = chunk.tickersInLevel.get(pos);
if (tickingWrapper != null && !tickingWrapper.isRemoved()) {
if (tickingWrapper.ticker instanceof BoundTickingBlockEntity tickingBE) {
//In general this should always be the case, so we inline some of the logic
// to optimize the calls to try and make extra ticks as cheap as possible
if (chunk.isTicking(pos)) {
ProfilerFiller profiler = level.getProfiler();
profiler.push(tickingWrapper::getType);
BlockState state = chunk.getBlockState(pos);
if (blockEntity.getType().isValid(state)) {
for (int i = 0; i < bonusTicks; i++) {
tickingBE.ticker.tick(level, pos, state, blockEntity);
}
}
profiler.pop();
}
} else {
//Fallback to just trying to make it tick extra
for (int i = 0; i < bonusTicks; i++) {
tickingWrapper.tick();
}
}
// optimistically check if the block entity is already sped up and if so, just increase the bonus ticks
if (levelBlockEntityTicks.containsKey(blockEntity.getBlockPos().asLong())) {
levelBlockEntityTicks.get(blockEntity.getBlockPos().asLong()).addTicks(bonusTicks);
} else {
if (!blockEntity.isRemoved() &&
!BlockEntities.BLACKLIST_TIME_WATCH_LOOKUP.contains(blockEntity.getType())) {
BlockPos pos = blockEntity.getBlockPos();
if (level.shouldTickBlocksAt(ChunkPos.asLong(pos))) {
levelBlockEntityTicks.put(pos.asLong(), new SpedUpBlockEntity(blockEntity, bonusTicks));
}
}
}
Expand All @@ -171,22 +160,101 @@ private void speedUpRandomTicks(Level level, int bonusTicks, AABB bBox) {
// Sanity check the box for chunk unload weirdness
return;
}
Long2ObjectOpenHashMap<SpedUpRandomBlock> levelRandomTicks = queuedRandomTicks.computeIfAbsent(level, k -> new Long2ObjectOpenHashMap<>());
for (BlockPos pos : WorldHelper.getPositionsFromBox(bBox)) {
if (WorldHelper.isBlockLoaded(serverLevel, pos)) {
BlockState state = serverLevel.getBlockState(pos);
Block block = state.getBlock();
if (state.isRandomlyTicking() && !state.is(PETags.Blocks.BLACKLIST_TIME_WATCH)
&& !(block instanceof LiquidBlock) // Don't speed non-source fluid blocks - dupe issues
&& !(block instanceof BonemealableBlock) && !(block instanceof IPlantable)) {// All plants should be sped using Harvest Goddess
pos = pos.immutable();
for (int i = 0; i < bonusTicks; i++) {
state.randomTick(serverLevel, pos, serverLevel.random);
// optimistically check if the block is already sped up and if so, just increase the bonus ticks
if (levelRandomTicks.containsKey(pos.asLong())) {
levelRandomTicks.get(pos.asLong()).addTicks(bonusTicks);
} else {
if (WorldHelper.isBlockLoaded(serverLevel, pos)) {
BlockState state = serverLevel.getBlockState(pos);
Block block = state.getBlock();
if (state.isRandomlyTicking() && !state.is(PETags.Blocks.BLACKLIST_TIME_WATCH)
&& !(block instanceof LiquidBlock) // Don't speed non-source fluid blocks - dupe issues
&& !(block instanceof BonemealableBlock) && !(block instanceof IPlantable)) {// All plants should be sped using Harvest Goddess
pos = pos.immutable();
levelRandomTicks.put(pos.asLong(), new SpedUpRandomBlock(serverLevel, pos, state, bonusTicks));
}
}
}
}
}

public static void completeTick(Level level, BooleanSupplier haveTime) {
long currentTime = System.currentTimeMillis();
double maxTPSLost = ProjectEConfig.server.effects.maxTPSLoss.get();
double asMS;
if (maxTPSLost >= 20 || maxTPSLost < 0) {
asMS = 10000;
} else {
asMS = 1000d / (20 - maxTPSLost) - 50;
}
// we now need to turn the 2 queues into a schedule that overshoots by the same ratio of ticks done to ticks scheduled for each
// we start by combining the 2 queues into a single one
// add up the total ticks,
// and allocate time accordingly

int totalTicks = 0;
int divisor = -1;
List<SpedUpItem> queue = new ArrayList<>();
Long2ObjectOpenHashMap<TimeWatch.SpedUpBlockEntity> qBE = queuedBlockEntityTicks.get(level);
if (qBE != null) {
for (SpedUpItem item : qBE.values()) {
queue.add(item);
if (divisor == -1 || item.getTicksLeft() < divisor) {
divisor = item.getTicksLeft();
}
totalTicks += item.getTicksLeft();
}
qBE.clear();
}
Long2ObjectOpenHashMap<TimeWatch.SpedUpRandomBlock> qRT = queuedRandomTicks.get(level);
if (qRT != null) {
for (SpedUpItem item : qRT.values()) {
queue.add(item);
if (divisor == -1 || item.getTicksLeft() < divisor) {
divisor = item.getTicksLeft();
}
totalTicks += item.getTicksLeft();
}
qRT.clear();
}
Set<SpedUpItem> toRemove = new HashSet<>();

int ticksTot = 0;

while (!queue.isEmpty()) {
if (haveTime.getAsBoolean()) currentTime = System.currentTimeMillis();
else if (System.currentTimeMillis() - currentTime > asMS) {
// we have run out of time, so we will drop the rest of the queue
// System.out.println("TimeWatch: Ran out of time, dropping " + (totalTicks - ticksTot) + " extra block ticks");
break;
}

// now we run for ticksleft / divisor ticks
for (SpedUpItem item : queue) {

// reasonable range so a single doesn't hog too much
int ticks = Mth.clamp(item.getTicksLeft() / divisor, 1, Math.min(totalTicks, 1_000));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's probably a better function usable here,

the idea is to get a ratio so that things that have 100 bonus ticks don't get the same as things with only like 10. but we also don't want this while loop to take too long for a single iteration, since it only checks if it's out of time each loop


for (int i = 0; i < ticks; i++) {
if (item.tick()) {
toRemove.add(item);
break;
}
}

ticksTot += ticks;
}

// remove finished
for (SpedUpItem item : toRemove) {
queue.remove(item);
}
toRemove.clear();
}
}

private ILangEntry getTimeName(ItemStack stack) {
byte mode = getTimeBoost(stack);
return switch (mode) {
Expand Down Expand Up @@ -273,4 +341,99 @@ public int getBarWidth(@NotNull ItemStack stack) {
public int getBarColor(@NotNull ItemStack stack) {
return getColorForBar(stack);
}

private interface SpedUpItem extends Comparable<SpedUpItem> {
boolean tick();
int getTicksLeft();
}

private static class SpedUpBlockEntity implements SpedUpItem {
private final BlockEntity entity;
private int ticksLeft;
@Nullable
private Runnable onTick;

public SpedUpBlockEntity(BlockEntity entity, int ticks) {
this.entity = entity;
this.ticksLeft = ticks;
}

public void addTicks(int ticks) {
ticksLeft += ticks;
}

public int getTicksLeft() {
return ticksLeft;
}

public boolean tick() {
if (onTick != null) {
onTick.run();
return --ticksLeft == 0;
}
Level level = entity.getLevel();
BlockPos pos = entity.getBlockPos();
LevelChunk chunk = level.getChunkAt(pos);
RebindableTickingBlockEntityWrapper tickingWrapper = chunk.tickersInLevel.get(pos);
if (tickingWrapper != null && !tickingWrapper.isRemoved()) {
if (tickingWrapper.ticker instanceof BoundTickingBlockEntity tickingBE) {
//In general this should always be the case, so we inline some of the logic
// to optimize the calls to try and make extra ticks as cheap as possible
if (chunk.isTicking(pos)) {
ProfilerFiller profiler = level.getProfiler();
profiler.push(tickingWrapper::getType);
BlockState state = chunk.getBlockState(pos);
if (entity.getType().isValid(state)) {
onTick = () -> tickingBE.ticker.tick(level, pos, state, entity);
}
profiler.pop();
}
} else {
//Fallback to just trying to make it tick extra
onTick = tickingWrapper::tick;
}
}
onTick.run();
return --ticksLeft == 0;
}

@Override
public int compareTo(SpedUpItem o) {
return Integer.compare(getTicksLeft(), o.getTicksLeft());
}

}

public static class SpedUpRandomBlock implements SpedUpItem {
private final BlockPos pos;
private final BlockState state;
private final ServerLevel serverLevel;
private int ticksLeft;

public SpedUpRandomBlock(ServerLevel level, BlockPos pos, BlockState state, int ticks) {
this.serverLevel = level;
this.pos = pos;
this.state = state;
this.ticksLeft = ticks;
}

public void addTicks(int ticks) {
ticksLeft += ticks;
}

public int getTicksLeft() {
return ticksLeft;
}

public boolean tick() {
state.randomTick(serverLevel, pos, serverLevel.random);
return --ticksLeft == 0;
}

@Override
public int compareTo(@NotNull TimeWatch.SpedUpItem o) {
return Integer.compare(getTicksLeft(), o.getTicksLeft());
}

}
}