DREAMFIRE Docs ← Back to site
Loading...
Searching...
No Matches
DreamBlockMask.java
Go to the documentation of this file.
1/*
2 * MIT License
3 *
4 * Copyright (c) 2025 Dreamfire Studio
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24package com.dreamfirestudios.dreamcore.DreamBlockMask;
25
26import com.dreamfirestudios.dreamcore.DreamCore;
27import lombok.Getter;
28import org.bukkit.Location;
29import org.bukkit.Material;
30import org.bukkit.block.BlockState;
31import org.bukkit.entity.Player;
32import org.bukkit.util.Vector;
33
34import java.util.Collections;
35import java.util.HashMap;
36import java.util.Map;
37
60public class DreamBlockMask {
61
62 private Player player;
63
64 @Getter private boolean deleteMaskOnNull;
65 @Getter private boolean ignoreAir;
66 @Getter private boolean resetLastFrames;
67 @Getter private boolean keepTrailTheSame;
68 @Getter private double minDistance;
69 @Getter private double maxX;
70 @Getter private double maxY;
71 @Getter private double maxZ;
72
73 @Getter private Map<Material, Material> blockExceptions;
74
75 private Map<Vector, BlockState> lastFrameBlockStates = new HashMap<>();
76 private final Map<Vector, BlockState> visitedTrailLocations = new HashMap<>();
77
81 @Getter
82 private volatile boolean actionBarPaused = true;
83
84 private DreamBlockMask() { }
85
86 // ----------------------------- Mutation helpers -----------------------------
87
93 public void addToExceptions(Map<Material, Material> blockExceptions){
94 if (blockExceptions == null) throw new IllegalArgumentException("Block exceptions cannot be null.");
95 this.blockExceptions = mergeExceptions(this.blockExceptions, blockExceptions);
96 }
97
98 // ----------------------------- Frame cycle -----------------------------
99
109 public boolean displayNextFrame(){
110 if (player == null || !player.isOnline() || actionBarPaused) return true;
111
112 final Location playerLoc = player.getLocation();
113 final double px = playerLoc.getX();
114 final double py = playerLoc.getY();
115 final double pz = playerLoc.getZ();
116
117 final Map<Vector, BlockState> previousFrameStates = new HashMap<>();
118 final Map<Vector, BlockState> newFrameStates = new HashMap<>();
119
120 // NOTE: Iterate using doubles to preserve exact behavior of your original logic.
121 for (double x = px - maxX; x < px + maxX; x++){
122 for (double y = py - maxY; y < py + maxY; y++){
123 for (double z = pz - maxZ; z < pz + maxZ; z++){
124
125 final Location loc = new Location(player.getWorld(), x, y, z);
126 if (loc.distance(playerLoc) < minDistance) continue;
127
128 final var block = loc.getBlock();
129 if (block == null) continue; // defensive
130 if (block.getType() == Material.AIR && ignoreAir) continue;
131
132 final Material viewMat = blockExceptions.getOrDefault(block.getType(), Material.BARRIER);
133 if (viewMat == null) continue;
134
135 final Vector key = block.getLocation().toVector();
136 previousFrameStates.put(key, block.getState());
137
138 final BlockState newState = block.getState();
139 newState.setType(viewMat);
140 newFrameStates.put(key, newState);
141
142 if (keepTrailTheSame && !visitedTrailLocations.containsKey(key)) {
143 visitedTrailLocations.put(key, newState);
144 }
145 }
146 }
147 }
148
149 // Restore areas from last frame that are no longer present (unless retained in the trail map).
150 if (resetLastFrames) {
151 for (Map.Entry<Vector, BlockState> e : lastFrameBlockStates.entrySet()) {
152 if (!newFrameStates.containsKey(e.getKey()) && !visitedTrailLocations.containsKey(e.getKey())) {
153 newFrameStates.put(e.getKey(), e.getValue());
154 }
155 }
156 }
157
158 // Pre-apply event hook
159 new BlockMaskFrameComputedEvent(player, this,
160 Collections.unmodifiableMap(newFrameStates),
161 Collections.unmodifiableMap(previousFrameStates));
162
163 // Apply and swap
164 player.sendBlockChanges(newFrameStates.values());
165 lastFrameBlockStates = previousFrameStates;
166
167 // Post-apply event hook
168 new BlockMaskFrameAppliedEvent(player, this,
169 Collections.unmodifiableMap(newFrameStates));
170
171 return false;
172 }
173
177 public void pause() {
178 if (!actionBarPaused) {
179 actionBarPaused = true;
180 // restore what the player currently sees for last frame and persistent trail
181 player.sendBlockChanges(lastFrameBlockStates.values());
182 player.sendBlockChanges(visitedTrailLocations.values());
183 new BlockMaskPausedEvent(player, this);
184 }
185 }
186
190 public void play() {
191 if (actionBarPaused) {
192 actionBarPaused = false;
193 new BlockMaskStartedEvent(player, this);
194 }
195 }
196
207 actionBarPaused = true;
208 player.sendBlockChanges(lastFrameBlockStates.values());
209 player.sendBlockChanges(visitedTrailLocations.values());
210 new BlockMaskStoppedEvent(player, this);
211 return DreamCore.DreamBlockMasks.remove(player.getUniqueId());
212 }
213
214 // ----------------------------- Utilities -----------------------------
215
219 public Map<Vector, BlockState> getVisitedTrailLocationsView() {
220 return Collections.unmodifiableMap(visitedTrailLocations);
221 }
222
223 private static Map<Material, Material> mergeExceptions(Map<Material, Material> base, Map<Material, Material> add) {
224 Map<Material, Material> out = new HashMap<>(base);
225 out.putAll(add);
226 return Collections.unmodifiableMap(out);
227 }
228
229 // ----------------------------- Builder -----------------------------
230
234 public static class Builder {
235
236 private final Map<Material, Material> blockExceptions = new HashMap<>();
237 private boolean deleteMaskOnNull = false;
238 private boolean ignoreAir = true;
239 private boolean resetLastFrames = true;
240 private boolean keepTrailTheSame = false;
241 private double minDistance = 0.0d;
242 private double maxX = 5.0d;
243 private double maxY = 5.0d;
244 private double maxZ = 5.0d;
245
251 public Builder blockExceptions(Map<Material, Material> blockExceptions){
252 if (blockExceptions == null) throw new IllegalArgumentException("Block exceptions cannot be null.");
253 this.blockExceptions.putAll(blockExceptions);
254 return this;
255 }
256
263 public Builder blockExceptions(Material target, Material view){
264 if (target == null || view == null) throw new IllegalArgumentException("Block type cannot be null.");
265 this.blockExceptions.put(target, view);
266 return this;
267 }
268
274 public Builder deleteMaskOnNull(boolean deleteMaskOnNull){
275 this.deleteMaskOnNull = deleteMaskOnNull;
276 return this;
277 }
278
284 public Builder ignoreAir(boolean ignoreAir){
285 this.ignoreAir = ignoreAir;
286 return this;
287 }
288
294 public Builder resetLastFrames(boolean resetLastFrames){
295 this.resetLastFrames = resetLastFrames;
296 return this;
297 }
298
304 public Builder keepTrailTheSame(boolean keepTrailTheSame){
305 this.keepTrailTheSame = keepTrailTheSame;
306 return this;
307 }
308
314 public Builder minDistance(double minDistance){
315 if (minDistance < 0) throw new IllegalArgumentException("minDistance cannot be negative.");
316 this.minDistance = minDistance;
317 return this;
318 }
319
323 public Builder maxX(double maxX){
324 if (maxX < 0) throw new IllegalArgumentException("Max X cannot be negative.");
325 this.maxX = maxX;
326 return this;
327 }
328
332 public Builder maxY(double maxY){
333 if (maxY < 0) throw new IllegalArgumentException("Max Y cannot be negative.");
334 this.maxY = maxY;
335 return this;
336 }
337
341 public Builder maxZ(double maxZ){
342 if (maxZ < 0) throw new IllegalArgumentException("Max Z cannot be negative.");
343 this.maxZ = maxZ;
344 return this;
345 }
346
356 public DreamBlockMask CreateMask(Player player){
357 if (player == null) throw new IllegalArgumentException("Player cannot be null.");
358 var stored = DreamCore.DreamBlockMasks.getOrDefault(player.getUniqueId(), null);
359 if (stored != null){
360 stored.addToExceptions(this.blockExceptions);
361 return stored;
362 }
363
364 DreamBlockMask mask = new DreamBlockMask();
365 mask.player = player;
366 mask.deleteMaskOnNull = deleteMaskOnNull;
367 mask.resetLastFrames = resetLastFrames;
368 mask.keepTrailTheSame = keepTrailTheSame;
369 mask.minDistance = minDistance;
370 mask.maxX = maxX;
371 mask.maxY = maxY;
372 mask.maxZ = maxZ;
373 mask.ignoreAir = ignoreAir;
374 mask.blockExceptions = Collections.unmodifiableMap(new HashMap<>(this.blockExceptions));
375
376 new BlockMaskCreatedEvent(mask, player);
377 // NOTE: As written, this returns the previous value (if any) — not the newly created mask.
378 // Left unchanged to avoid behavior changes without your approval.
379 return DreamCore.DreamBlockMasks.put(player.getUniqueId(), mask);
380 }
381 }
382}
Fired when a DreamBlockMask is created and registered for a player.
Fired after a frame’s block changes are sent to the player.
Fired after a frame is computed but before it is sent to the player.
Fired when a DreamBlockMask transitions from paused to playing.
Fired when a DreamBlockMask is stopped and unregistered.
Builder maxZ(double maxZ)
Set Z axis bound around the player for the mask region.
Builder keepTrailTheSame(boolean keepTrailTheSame)
Keep a persistent trail of masked blocks across frames.
Builder minDistance(double minDistance)
Minimum distance from the player under which blocks are not affected.
Builder resetLastFrames(boolean resetLastFrames)
Restore blocks from the previous frame if they are not in the new frame (unless retained by trail).
Builder deleteMaskOnNull(boolean deleteMaskOnNull)
Delete mask automatically when the player reference becomes invalid.
Builder blockExceptions(Map< Material, Material > blockExceptions)
Bulk add block exceptions (actual → view).
Builder blockExceptions(Material target, Material view)
Add a single block exception mapping.
Builder maxY(double maxY)
Set Y axis bound around the player for the mask region.
Builder maxX(double maxX)
Set X axis bound around the player for the mask region.
Builder ignoreAir(boolean ignoreAir)
Ignore AIR blocks when producing the mask.
DreamBlockMask CreateMask(Player player)
Creates (or merges with) the player's mask.
Renders a per-player “block mask” viewport by sending ephemeral block changes around the player.
DreamBlockMask stop()
Stops the mask, restores visible changes, fires stopped event, and removes it from the core registry.
boolean displayNextFrame()
Computes and applies the next frame.
Map< Vector, BlockState > getVisitedTrailLocationsView()
Returns an immutable view of the persistent trail entries.
void addToExceptions(Map< Material, Material > blockExceptions)
Adds/merges block type exceptions into this mask.
void pause()
Pauses the mask and restores any pending last/trail states to the player.
static final LinkedHashMap< UUID, DreamBlockMask > DreamBlockMasks