DREAMFIRE Docs ← Back to site
Loading...
Searching...
No Matches
DreamHologram.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.DreamHologram;
25
26import com.dreamfirestudios.dreamcore.DreamChat.DreamMessageFormatter;
27import com.dreamfirestudios.dreamcore.DreamChat.DreamMessageSettings;
28import com.dreamfirestudios.dreamcore.DreamCore;
29import com.dreamfirestudios.dreamcore.DreamJava.DreamClassID;
30import lombok.Getter;
31import net.kyori.adventure.text.Component;
32import org.bukkit.Bukkit;
33import org.bukkit.Location;
34import org.bukkit.World;
35import org.bukkit.entity.ArmorStand;
36import org.jetbrains.annotations.NotNull;
37import org.jetbrains.annotations.Nullable;
38
39import java.util.ArrayList;
40import java.util.List;
41import java.util.UUID;
42import java.util.function.Consumer;
43import java.util.function.Function;
44
55public class DreamHologram extends DreamClassID {
56
58 private final List<ArmorStand> armorStands = new ArrayList<>();
59
60 @Getter private String hologramName;
61 @Getter private Location startLocation;
62
63 @Getter private boolean visible = false;
64 @Getter private boolean customNameVisible = true;
65 @Getter private boolean useGravity = false;
66
71 @Getter private float gapBetweenLines = -0.5f;
72
77 private Function<Integer, Component> lineGenerator;
78
79 // -----------------------------------------------------------------------
80 // Query
81 // -----------------------------------------------------------------------
82
87 public boolean isArmorStand(@NotNull ArmorStand armorStand) {
88 return armorStands.contains(armorStand);
89 }
90
94 public int size() {
95 return armorStands.size();
96 }
97
104 @Nullable
105 public Component line(int index) {
106 if (index < 0 || index >= armorStands.size()) return null;
107 return armorStands.get(index).customName();
108 }
109
110 // -----------------------------------------------------------------------
111 // Mutations (MAIN THREAD)
112 // -----------------------------------------------------------------------
113
121 public void addNewLine(int index) {
122 ensureMainThread();
123 if (index < 0 || index > armorStands.size())
124 throw new IllegalArgumentException("Index out of bounds: " + index);
125 if (startLocation == null) return;
126 final World world = startLocation.getWorld();
127 if (world == null) return;
128
129 // Compute name via generator (null-safe)
130 final Component name = formatLine(index);
131
132 // Spawn and configure a new marker ArmorStand
133 final Location spawnLoc = lineLocation(index);
134 ArmorStand stand = world.spawn(spawnLoc, ArmorStand.class, configureArmorStand(name));
135
136 // Insert into list and re-stack positions beneath it
137 armorStands.add(index, stand);
138 restackFrom(index);
139
140 HologramAddLineEvent.fire(this, name);
142 }
143
150 public void editLine(int index) {
151 ensureMainThread();
152 if (index < 0 || index >= armorStands.size())
153 throw new IllegalArgumentException("Invalid line index: " + index);
154
155 final Component name = formatLine(index);
156 ArmorStand stand = armorStands.get(index);
157 stand.customName(name);
158 stand.setCustomNameVisible(customNameVisible);
159
160 HologramEditLineEvent.fire(this, index, name);
161 }
162
169 public void removeLine(int index) {
170 ensureMainThread();
171 if (index < 0 || index >= armorStands.size()) return;
172
173 ArmorStand stand = armorStands.remove(index);
174 stand.remove();
175
176 restackFrom(index);
177 HologramRemoveLineEvent.fire(this, index);
179 }
180
185 public void updateHologram() {
186 ensureMainThread();
187 restackFrom(0);
189 }
190
194 public void displayNextFrame() {
195 ensureMainThread();
196 for (int i = 0; i < armorStands.size(); i++) {
197 editLine(i);
198 }
199 }
200
205 public void deleteHologram() {
206 ensureMainThread();
207 for (ArmorStand stand : armorStands) {
208 stand.remove();
209 }
210 armorStands.clear();
212 DreamCore.DreamHolograms.remove(getClassID());
213 }
214
215 // -----------------------------------------------------------------------
216 // Helpers
217 // -----------------------------------------------------------------------
218
219 private void restackFrom(int startIndex) {
220 if (startLocation == null) return;
221 for (int i = startIndex; i < armorStands.size(); i++) {
222 ArmorStand stand = armorStands.get(i);
223 stand.teleport(lineLocation(i));
224 stand.setCustomNameVisible(customNameVisible);
225 stand.setGravity(useGravity);
226 stand.setInvisible(!visible);
227 }
228 }
229
230 @NotNull
231 private Location lineLocation(int index) {
232 return startLocation.clone().add(0.0, index * gapBetweenLines, 0.0);
233 }
234
235 @NotNull
236 private Component formatLine(int index) {
237 Component generated = lineGenerator != null
238 ? lineGenerator.apply(index)
239 : Component.empty();
240 return DreamMessageFormatter.format(generated, DreamMessageSettings.all());
241 }
242
243 @NotNull
244 private Consumer<ArmorStand> configureArmorStand(Component name) {
245 return stand -> {
246 // "Marker" stands have no hitbox; pure visual line
247 stand.setMarker(true);
248 stand.setInvisible(!visible);
249 stand.setGravity(useGravity);
250 stand.setCustomNameVisible(customNameVisible);
251 stand.customName(name);
252 stand.setSmall(true);
253 stand.setPersistent(true);
254 // Optional polish:
255 stand.setBasePlate(false);
256 stand.setArms(false);
257 stand.setCanMove(false); // Paper API
258 };
259 }
260
261 private static void ensureMainThread() {
262 if (!Bukkit.isPrimaryThread()) {
263 throw new IllegalStateException("Hologram mutations must run on the server main thread.");
264 }
265 }
266
267 // -----------------------------------------------------------------------
268 // Builder
269 // -----------------------------------------------------------------------
270
274 public static class HologramBuilder {
275 private String hologramName = UUID.randomUUID().toString();
276 private boolean visible = false;
277 private boolean customNameVisible = true;
278 private boolean useGravity = false;
279 private float gapBetweenLines = -0.5f;
280 private int linesToAdd = 0;
281
282 public HologramBuilder hologramName(@NotNull String hologramName) {
283 this.hologramName = hologramName;
284 return this;
285 }
286
287 public HologramBuilder lines(int linesToAdd) {
288 if (linesToAdd < 0) throw new IllegalArgumentException("linesToAdd cannot be negative");
289 this.linesToAdd = linesToAdd;
290 return this;
291 }
292
293 public HologramBuilder visible(boolean visible) {
294 this.visible = visible;
295 return this;
296 }
297
298 public HologramBuilder customNameVisible(boolean customNameVisible) {
299 this.customNameVisible = customNameVisible;
300 return this;
301 }
302
303 public HologramBuilder useGravity(boolean useGravity) {
304 this.useGravity = useGravity;
305 return this;
306 }
307
308 public HologramBuilder gapBetweenLines(float gapBetweenLines) {
309 this.gapBetweenLines = gapBetweenLines;
310 return this;
311 }
312
320 public DreamHologram create(@NotNull Location location,
321 @NotNull Function<Integer, Component> lineGenerator) {
322 ensureMainThread();
323 World world = location.getWorld();
324 if (world == null) throw new IllegalArgumentException("Location must have a world");
325
326 DreamHologram hologram = new DreamHologram();
327 hologram.hologramName = hologramName;
328 hologram.startLocation = location.clone();
329 hologram.visible = visible;
330 hologram.customNameVisible = customNameVisible;
331 hologram.useGravity = useGravity;
332 hologram.gapBetweenLines = gapBetweenLines;
333 hologram.lineGenerator = lineGenerator;
334
335 for (int i = 0; i < linesToAdd; i++) {
336 hologram.addNewLine(i);
337 }
338
339 DreamCore.DreamHolograms.put(hologram.getClassID(), hologram);
340 HologramSpawnEvent.fire(hologram);
341 return hologram;
342 }
343 }
344}
static final LinkedHashMap< UUID, DreamHologram > DreamHolograms
DreamHologram create(@NotNull Location location, @NotNull Function< Integer, Component > lineGenerator)
Creates and spawns a new hologram at the given location.
A multi-line Adventure Component hologram backed by stacked ArmorStands.
void addNewLine(int index)
Inserts a new line at the given index and spawns a configured ArmorStand.
void displayNextFrame()
Re-applies the line generator to all lines (useful for animated text).
void updateHologram()
Teleports all line ArmorStands to their correct stacked positions from top to bottom.
void deleteHologram()
Deletes the entire hologram and removes all ArmorStands.
void removeLine(int index)
Removes a line at the specified index and deletes its ArmorStand.
Component line(int index)
Returns the Component name of a line.
boolean isArmorStand(@NotNull ArmorStand armorStand)
void editLine(int index)
Updates the content (custom name) of an existing line.
Event fired after a hologram line has been inserted and its ArmorStand spawned.
static void fire(@NotNull DreamHologram hologram, @NotNull Component line)
Convenience helper to create and call this event via the plugin manager.
Event fired after a hologram has been deleted and all ArmorStands removed.
static void fire(@NotNull DreamHologram hologram)
Fires this event through the Bukkit plugin manager.
Event fired after a hologram line's content (custom name) has been updated.
static void fire(@NotNull DreamHologram hologram, int index, @NotNull Component line)
Fires this event via the plugin manager.
Event fired after a line has been removed from a hologram.
static void fire(@NotNull DreamHologram hologram, int index)
Fires this event via the Bukkit plugin manager.
Event fired after a hologram has been created and its initial lines spawned.
static void fire(@NotNull DreamHologram hologram)
Fires this event via the Bukkit plugin manager.
Event fired after a hologram has been collectively updated (e.g., re-stack/teleport,...
static void fire(@NotNull DreamHologram hologram)
Fires this event via the Bukkit plugin manager.