Skip to content

Commit 8aa5e53

Browse files
temporal rewinding? (removed sculk mixins too)
1 parent 297055d commit 8aa5e53

13 files changed

Lines changed: 327 additions & 63 deletions

src/main/java/net/tcmfatbird/tutorialmod/TutorialMod.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import net.tcmfatbird.tutorialmod.feature.BlockHighlightTracker;
1616
import net.tcmfatbird.tutorialmod.feature.ChatMentions;
1717
import net.tcmfatbird.tutorialmod.feature.UraniumRadiationHandler;
18+
import net.tcmfatbird.tutorialmod.feature.TemporalRewindManager;
1819
import net.tcmfatbird.tutorialmod.network.*;
1920
import net.tcmfatbird.tutorialmod.item.ModItemGroups;
2021
import net.tcmfatbird.tutorialmod.item.ModItems;
@@ -40,15 +41,21 @@ public class TutorialMod implements ModInitializer {
4041
public void onInitialize() {
4142
MutationEnchantment.register();
4243
PayloadTypeRegistry.playC2S().register(SetTimePacket.ID, SetTimePacket.CODEC);
44+
PayloadTypeRegistry.playC2S().register(TemporalRewindTogglePacket.ID, TemporalRewindTogglePacket.CODEC);
4345
PayloadTypeRegistry.playS2C().register(RadiationLevelPacket.ID, RadiationLevelPacket.CODEC);
4446
PayloadTypeRegistry.playS2C().register(NearestUraniumPacket.ID, NearestUraniumPacket.CODEC);
47+
PayloadTypeRegistry.playS2C().register(TemporalRewindStatePacket.ID, TemporalRewindStatePacket.CODEC);
4548

4649
ServerPlayNetworking.registerGlobalReceiver(SetTimePacket.ID, (payload, context) -> {
4750
context.server().execute(() -> {
4851
context.player().getServerWorld().setTimeOfDay(payload.time());
4952
});
5053
});
5154

55+
ServerPlayNetworking.registerGlobalReceiver(TemporalRewindTogglePacket.ID, (payload, context) ->
56+
context.server().execute(() -> TemporalRewindManager.setRewinding(context.player(), payload.rewinding()))
57+
);
58+
5259
PayloadTypeRegistry.playS2C().register(ClockTogglePacket.ID, ClockTogglePacket.CODEC);
5360
PayloadTypeRegistry.playS2C().register(BlockHighlightPacket.ID, BlockHighlightPacket.CODEC);
5461

@@ -57,6 +64,7 @@ public void onInitialize() {
5764
ModBlocks.registerModBlocks();
5865
ModOreGeneration.register();
5966
UraniumRadiationHandler.register();
67+
TemporalRewindManager.register();
6068
CustomCommands.register();
6169

6270
FuelRegistry.INSTANCE.add(ModItems.STARLIGHT_ASHES, 600);

src/main/java/net/tcmfatbird/tutorialmod/TutorialModClient.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,35 @@
44
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
55
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
66
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
7-
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
87
import net.minecraft.client.option.KeyBinding;
98
import net.minecraft.text.Text;
109
import net.minecraft.util.Formatting;
1110
import net.tcmfatbird.tutorialmod.feature.BlockHighlightRenderer;
1211
import net.tcmfatbird.tutorialmod.feature.GeigerCounterClient;
1312
import net.tcmfatbird.tutorialmod.feature.GeigerHud;
13+
import net.tcmfatbird.tutorialmod.feature.TemporalRewindClientEffects;
1414
import net.tcmfatbird.tutorialmod.gui.ClockScreen;
1515
import net.tcmfatbird.tutorialmod.item.ModItems;
1616
import net.tcmfatbird.tutorialmod.network.BlockHighlightPacket;
1717
import net.tcmfatbird.tutorialmod.network.ClockTogglePacket;
1818
import net.tcmfatbird.tutorialmod.network.NearestUraniumPacket;
1919
import net.tcmfatbird.tutorialmod.network.RadiationLevelPacket;
20+
import net.tcmfatbird.tutorialmod.network.TemporalRewindStatePacket;
21+
import net.tcmfatbird.tutorialmod.network.TemporalRewindTogglePacket;
2022
import org.lwjgl.glfw.GLFW;
2123

2224
public class TutorialModClient implements ClientModInitializer {
2325
private int tickCounter = 0;
2426
private boolean clockEnabled = true;
2527

2628
private static KeyBinding clockGuiKey;
29+
private static KeyBinding temporalRewindKey;
30+
private boolean wasRewindPressed = false;
2731

2832
@Override
2933
public void onInitializeClient() {
3034
GeigerHud.register();
35+
TemporalRewindClientEffects.register();
3136

3237
ClientPlayNetworking.registerGlobalReceiver(NearestUraniumPacket.ID, (payload, context) -> {
3338
context.client().execute(() -> {
@@ -42,6 +47,12 @@ public void onInitializeClient() {
4247
"key.category.tutorialmod"
4348
));
4449

50+
temporalRewindKey = KeyBindingHelper.registerKeyBinding(new KeyBinding(
51+
"key.tutorialmod.temporal_rewind",
52+
GLFW.GLFW_KEY_R,
53+
"key.category.tutorialmod"
54+
));
55+
4556
ClientPlayNetworking.registerGlobalReceiver(RadiationLevelPacket.ID, (payload, context) -> {
4657
context.client().execute(() -> {
4758
GeigerCounterClient.setRadiationLevel(payload.level());
@@ -71,6 +82,10 @@ public void onInitializeClient() {
7182
});
7283
});
7384

85+
ClientPlayNetworking.registerGlobalReceiver(TemporalRewindStatePacket.ID, (payload, context) -> {
86+
context.client().execute(() -> TemporalRewindClientEffects.setRewinding(payload.rewinding()));
87+
});
88+
7489
// --- TICK ---
7590
ClientTickEvents.END_CLIENT_TICK.register(client -> {
7691

@@ -88,6 +103,7 @@ public void onInitializeClient() {
88103
}
89104

90105
BlockHighlightRenderer.tick();
106+
TemporalRewindClientEffects.tick(client);
91107

92108
// Open clock GUI on keypress
93109
while (clockGuiKey.wasPressed()) {
@@ -96,6 +112,14 @@ public void onInitializeClient() {
96112
}
97113
}
98114

115+
boolean rewindPressed = temporalRewindKey.isPressed();
116+
if (client.player != null && rewindPressed != wasRewindPressed) {
117+
ClientPlayNetworking.send(new TemporalRewindTogglePacket(rewindPressed));
118+
wasRewindPressed = rewindPressed;
119+
} else if (client.player == null) {
120+
wasRewindPressed = false;
121+
}
122+
99123
// Clock display
100124
if (!clockEnabled) return;
101125
if (client.world == null || client.player == null) return;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package net.tcmfatbird.tutorialmod.feature;
2+
3+
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
4+
import net.minecraft.client.MinecraftClient;
5+
import net.minecraft.client.gui.DrawContext;
6+
import net.minecraft.client.render.RenderTickCounter;
7+
import net.minecraft.sound.SoundCategory;
8+
import net.minecraft.sound.SoundEvents;
9+
10+
public final class TemporalRewindClientEffects {
11+
private static boolean rewinding;
12+
private static int soundTick;
13+
private static int overlayTick;
14+
15+
private TemporalRewindClientEffects() {}
16+
17+
public static void register() {
18+
HudRenderCallback.EVENT.register(TemporalRewindClientEffects::render);
19+
}
20+
21+
public static void setRewinding(boolean active) {
22+
rewinding = active;
23+
if (!active) {
24+
soundTick = 0;
25+
overlayTick = 0;
26+
}
27+
}
28+
29+
public static void tick(MinecraftClient client) {
30+
if (!rewinding || client.player == null || client.world == null) {
31+
return;
32+
}
33+
34+
overlayTick++;
35+
soundTick++;
36+
37+
if (soundTick >= 6) {
38+
soundTick = 0;
39+
float pitch = (overlayTick / 6 % 2 == 0) ? 1.8f : 0.55f;
40+
client.world.playSound(
41+
client.player.getX(),
42+
client.player.getY(),
43+
client.player.getZ(),
44+
SoundEvents.BLOCK_RESPAWN_ANCHOR_CHARGE,
45+
SoundCategory.PLAYERS,
46+
0.35f,
47+
pitch,
48+
false
49+
);
50+
}
51+
}
52+
53+
private static void render(DrawContext context, RenderTickCounter tickCounter) {
54+
if (!rewinding) {
55+
return;
56+
}
57+
58+
MinecraftClient client = MinecraftClient.getInstance();
59+
if (client.player == null || client.world == null) {
60+
return;
61+
}
62+
63+
int width = context.getScaledWindowWidth();
64+
int height = context.getScaledWindowHeight();
65+
int pulse = 55 + (overlayTick % 20);
66+
int overlayColor = (pulse << 24) | 0x2233AA;
67+
68+
context.fill(0, 0, width, height, overlayColor);
69+
context.drawText(client.textRenderer, "⏪ REWINDING", width / 2 - 45, height / 2 - 35, 0x99BBFF, true);
70+
}
71+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package net.tcmfatbird.tutorialmod.feature;
2+
3+
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
4+
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
5+
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
6+
import net.minecraft.block.BlockState;
7+
import net.minecraft.item.Item;
8+
import net.minecraft.item.ItemStack;
9+
import net.minecraft.item.Items;
10+
import net.minecraft.server.network.ServerPlayerEntity;
11+
import net.minecraft.server.world.ServerWorld;
12+
import net.minecraft.util.math.BlockPos;
13+
import net.tcmfatbird.tutorialmod.item.ModItems;
14+
import net.tcmfatbird.tutorialmod.network.TemporalRewindStatePacket;
15+
16+
import java.util.ArrayDeque;
17+
import java.util.Deque;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.UUID;
21+
22+
public final class TemporalRewindManager {
23+
private static final int MAX_POSITION_HISTORY = 20 * 60 * 5;
24+
25+
private static final Map<UUID, PlayerTimeline> TIMELINES = new HashMap<>();
26+
27+
private TemporalRewindManager() {}
28+
29+
public static void register() {
30+
ServerTickEvents.END_SERVER_TICK.register(server -> {
31+
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
32+
PlayerTimeline timeline = TIMELINES.computeIfAbsent(player.getUuid(), id -> new PlayerTimeline());
33+
34+
if (timeline.rewinding) {
35+
rewindOneTick(player, timeline);
36+
} else {
37+
captureTick(player, timeline);
38+
}
39+
}
40+
});
41+
42+
PlayerBlockBreakEvents.BEFORE.register((world, player, pos, state, blockEntity) -> {
43+
if (!(player instanceof ServerPlayerEntity serverPlayer)) {
44+
return true;
45+
}
46+
47+
PlayerTimeline timeline = TIMELINES.computeIfAbsent(serverPlayer.getUuid(), id -> new PlayerTimeline());
48+
if (!timeline.rewinding && hasRewindDevice(serverPlayer)) {
49+
timeline.brokenBlocks.addLast(new BrokenBlock(pos.toImmutable(), state));
50+
}
51+
return true;
52+
});
53+
}
54+
55+
public static void setRewinding(ServerPlayerEntity player, boolean rewinding) {
56+
PlayerTimeline timeline = TIMELINES.computeIfAbsent(player.getUuid(), id -> new PlayerTimeline());
57+
if (!hasRewindDevice(player)) {
58+
setRewindingState(player, timeline, false);
59+
return;
60+
}
61+
62+
setRewindingState(player, timeline, rewinding);
63+
}
64+
65+
private static void setRewindingState(ServerPlayerEntity player, PlayerTimeline timeline, boolean rewinding) {
66+
if (timeline.rewinding == rewinding) {
67+
return;
68+
}
69+
70+
timeline.rewinding = rewinding;
71+
ServerPlayNetworking.send(player, new TemporalRewindStatePacket(rewinding));
72+
}
73+
74+
private static void captureTick(ServerPlayerEntity player, PlayerTimeline timeline) {
75+
if (!hasRewindDevice(player)) {
76+
timeline.positions.clear();
77+
timeline.brokenBlocks.clear();
78+
setRewindingState(player, timeline, false);
79+
return;
80+
}
81+
82+
timeline.positions.addLast(new PlayerSnapshot(
83+
player.getX(),
84+
player.getY(),
85+
player.getZ(),
86+
player.getYaw(),
87+
player.getPitch(),
88+
player.getHealth(),
89+
player.getServerWorld().getTimeOfDay()
90+
));
91+
92+
while (timeline.positions.size() > MAX_POSITION_HISTORY) {
93+
timeline.positions.removeFirst();
94+
}
95+
}
96+
97+
private static void rewindOneTick(ServerPlayerEntity player, PlayerTimeline timeline) {
98+
if (!hasRewindDevice(player)) {
99+
setRewindingState(player, timeline, false);
100+
return;
101+
}
102+
103+
PlayerSnapshot snapshot = timeline.positions.pollLast();
104+
if (snapshot != null) {
105+
ServerWorld world = player.getServerWorld();
106+
player.teleport(world, snapshot.x, snapshot.y, snapshot.z, snapshot.yaw, snapshot.pitch);
107+
world.setTimeOfDay(snapshot.timeOfDay);
108+
player.setHealth(Math.min(player.getMaxHealth(), snapshot.health));
109+
}
110+
111+
BrokenBlock broken = timeline.brokenBlocks.pollLast();
112+
if (broken != null && player.getServerWorld().getBlockState(broken.pos()).isAir()) {
113+
if (consumeRequiredBlockItem(player, broken.state())) {
114+
player.getServerWorld().setBlockState(broken.pos(), broken.state(), 3);
115+
}
116+
}
117+
118+
if (timeline.positions.isEmpty() && timeline.brokenBlocks.isEmpty()) {
119+
setRewindingState(player, timeline, false);
120+
}
121+
}
122+
123+
private static boolean consumeRequiredBlockItem(ServerPlayerEntity player, BlockState state) {
124+
Item requiredItem = state.getBlock().asItem();
125+
if (requiredItem == Items.AIR) {
126+
return false;
127+
}
128+
129+
for (int slot = 0; slot < player.getInventory().size(); slot++) {
130+
ItemStack stack = player.getInventory().getStack(slot);
131+
if (stack.isOf(requiredItem)) {
132+
stack.decrement(1);
133+
return true;
134+
}
135+
}
136+
137+
return false;
138+
}
139+
140+
private static boolean hasRewindDevice(ServerPlayerEntity player) {
141+
for (int i = 0; i < player.getInventory().size(); i++) {
142+
if (player.getInventory().getStack(i).isOf(ModItems.TEMPORAL_REWINDER)) {
143+
return true;
144+
}
145+
}
146+
return false;
147+
}
148+
149+
private record PlayerSnapshot(double x, double y, double z, float yaw, float pitch, float health, long timeOfDay) {}
150+
151+
private record BrokenBlock(BlockPos pos, BlockState state) {}
152+
153+
private static final class PlayerTimeline {
154+
private final Deque<PlayerSnapshot> positions = new ArrayDeque<>();
155+
private final Deque<BrokenBlock> brokenBlocks = new ArrayDeque<>();
156+
private boolean rewinding;
157+
}
158+
}

src/main/java/net/tcmfatbird/tutorialmod/item/ModItemGroups.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class ModItemGroups {
2929
entries.add(ModItems.STARLIGHT_ASHES);
3030

3131
entries.add(ModItems.GEIGER_COUNTER);
32+
entries.add(ModItems.TEMPORAL_REWINDER);
3233
}).build());
3334

3435
public static final ItemGroup PINK_GARNET_BLOCKS_GROUP = Registry.register(Registries.ITEM_GROUP,

0 commit comments

Comments
 (0)