steel_core/block_entity/entities/
sign.rs1use std::array;
7use std::sync::{Arc, Weak};
8
9use simdnbt::borrow::{
10 BaseNbtCompound as BorrowedNbtCompound, NbtCompound as BorrowedNbtCompoundView,
11};
12use simdnbt::owned::{NbtCompound, NbtList};
13use steel_registry::block_entity_type::BlockEntityTypeRef;
14use steel_registry::{DyeColor, vanilla_block_entity_types};
15use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
16use text_components::{TextComponent, content::Content};
17use uuid::Uuid;
18
19use crate::block_entity::{BlockEntity, BlockEntityBase};
20use crate::entity::Entity;
21use crate::world::World;
22
23const MAX_EDIT_DISTANCE: f64 = 4.0;
26
27pub const SIGN_LINES: usize = 4;
29
30#[derive(Debug, Clone)]
32pub struct SignText {
33 pub messages: [TextComponent; SIGN_LINES],
35 pub color: DyeColor,
37 pub has_glowing_text: bool,
39}
40
41impl Default for SignText {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl SignText {
48 #[must_use]
50 pub fn new() -> Self {
51 Self {
52 messages: array::from_fn(|_| TextComponent::new()),
53 color: DyeColor::Black,
54 has_glowing_text: false,
55 }
56 }
57
58 #[must_use]
60 pub fn get_message(&self, index: usize) -> Option<&TextComponent> {
61 self.messages.get(index)
62 }
63
64 pub fn set_message(&mut self, index: usize, message: TextComponent) {
66 if index < SIGN_LINES {
67 self.messages[index] = message;
68 }
69 }
70
71 #[must_use]
73 pub fn has_message(&self) -> bool {
74 self.messages.iter().any(|msg| {
75 match &msg.content {
77 Content::Text { text } => !text.is_empty(),
78 _ => true, }
80 })
81 }
82
83 pub fn load(&mut self, nbt: BorrowedNbtCompoundView<'_, '_>) {
85 if let Some(messages) = nbt.list("messages") {
86 let tags = messages.to_owned().as_nbt_tags();
87 let messages = tags
88 .iter()
89 .map(TextComponent::from_nbt)
90 .collect::<Option<Vec<_>>>();
91 if let Some(messages) = messages
92 && let Ok(messages) = <[TextComponent; SIGN_LINES]>::try_from(messages)
93 {
94 self.messages = messages;
95 }
96 }
97
98 if let Some(color_str) = nbt.string("color") {
100 self.color =
101 DyeColor::from_serialized_name(&color_str.to_str()).unwrap_or(DyeColor::Black);
102 }
103
104 if let Some(glow) = nbt.byte("has_glowing_text") {
106 self.has_glowing_text = glow != 0;
107 }
108 }
109
110 pub fn save(&self, nbt: &mut NbtCompound) {
112 nbt.insert(
113 "messages",
114 NbtList::from(
115 self.messages
116 .iter()
117 .map(TextComponent::to_codec_nbt)
118 .collect::<Vec<_>>(),
119 ),
120 );
121
122 nbt.insert("color", self.color.serialized_name());
124
125 nbt.insert("has_glowing_text", i8::from(self.has_glowing_text));
127 }
128}
129
130pub struct SignBlockEntity {
134 base: BlockEntityBase,
135 sign: SyncMutex<SignState>,
136}
137
138struct SignState {
139 front_text: SignText,
141 back_text: SignText,
143 is_waxed: bool,
145 player_who_may_edit: Option<Uuid>,
148}
149
150unsafe impl DowncastType for SignBlockEntity {
153 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/sign");
154}
155
156impl SignBlockEntity {
157 #[must_use]
159 pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
160 Self::with_type(level, &vanilla_block_entity_types::SIGN, pos, state)
161 }
162
163 #[must_use]
165 pub fn new_hanging(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
166 Self::with_type(level, &vanilla_block_entity_types::HANGING_SIGN, pos, state)
167 }
168
169 #[must_use]
171 pub fn with_type(
172 level: Weak<World>,
173 block_entity_type: BlockEntityTypeRef,
174 pos: BlockPos,
175 state: BlockStateId,
176 ) -> Self {
177 Self {
178 base: BlockEntityBase::new(block_entity_type, level, pos, state),
179 sign: SyncMutex::new(SignState {
180 front_text: SignText::new(),
181 back_text: SignText::new(),
182 is_waxed: false,
183 player_who_may_edit: None,
184 }),
185 }
186 }
187
188 #[must_use]
190 pub fn get_player_who_may_edit(&self) -> Option<Uuid> {
191 self.sign.lock().player_who_may_edit
192 }
193
194 pub fn set_player_who_may_edit(&self, player: Option<Uuid>) {
196 self.sign.lock().player_who_may_edit = player;
197 }
198
199 #[must_use]
201 pub fn is_other_player_editing(&self, player_uuid: Uuid) -> bool {
202 self.sign
203 .lock()
204 .player_who_may_edit
205 .is_some_and(|editor| editor != player_uuid)
206 }
207
208 #[must_use]
210 pub fn get_text(&self, front: bool) -> SignText {
211 let sign = self.sign.lock();
212 if front {
213 sign.front_text.clone()
214 } else {
215 sign.back_text.clone()
216 }
217 }
218
219 #[must_use]
221 pub fn is_waxed(&self) -> bool {
222 self.sign.lock().is_waxed
223 }
224
225 pub fn wax(&self) -> bool {
227 let mut sign = self.sign.lock();
228 if sign.is_waxed {
229 return false;
230 }
231 sign.is_waxed = true;
232 true
233 }
234 pub fn set_glowing(&self, front: bool, glowing: bool) -> bool {
239 let mut sign = self.sign.lock();
240 let text = if front {
241 &mut sign.front_text
242 } else {
243 &mut sign.back_text
244 };
245 if text.has_glowing_text == glowing {
246 return false;
247 }
248 text.has_glowing_text = glowing;
249 true
250 }
251
252 pub fn set_text(&self, text: SignText, front: bool) {
254 let mut sign = self.sign.lock();
255 if front {
256 sign.front_text = text;
257 } else {
258 sign.back_text = text;
259 }
260 }
261}
262
263impl BlockEntity for SignBlockEntity {
264 fn base(&self) -> &BlockEntityBase {
265 &self.base
266 }
267
268 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
269 let nbt_view: BorrowedNbtCompoundView<'_, '_> = nbt.into();
271 let mut sign = self.sign.lock();
272
273 if let Some(front_nbt) = nbt_view.compound("front_text") {
275 sign.front_text.load(front_nbt);
276 }
277
278 if let Some(back_nbt) = nbt_view.compound("back_text") {
280 sign.back_text.load(back_nbt);
281 }
282
283 if let Some(waxed) = nbt_view.byte("is_waxed") {
285 sign.is_waxed = waxed != 0;
286 }
287 }
288
289 fn save_additional(&self, nbt: &mut NbtCompound) {
290 let sign = self.sign.lock();
291 let mut front_nbt = NbtCompound::new();
293 sign.front_text.save(&mut front_nbt);
294 nbt.insert("front_text", front_nbt);
295
296 let mut back_nbt = NbtCompound::new();
298 sign.back_text.save(&mut back_nbt);
299 nbt.insert("back_text", back_nbt);
300
301 nbt.insert("is_waxed", i8::from(sign.is_waxed));
303 }
304
305 fn get_update_tag(&self) -> Option<NbtCompound> {
306 let mut nbt = NbtCompound::new();
308 self.save_additional(&mut nbt);
309 Some(nbt)
310 }
311
312 fn tick(&self, world: &Arc<World>) {
313 let editor_uuid = self.sign.lock().player_who_may_edit;
315 let Some(editor_uuid) = editor_uuid else {
316 return;
317 };
318 let should_clear = world
319 .players
320 .get_by_uuid(&editor_uuid)
321 .is_none_or(|player| {
322 let pos = self.get_block_pos();
323 let player_pos = player.position();
324 let dx = player_pos.x - f64::from(pos.0.x) - 0.5;
325 let dy = player_pos.y - f64::from(pos.0.y) - 0.5;
326 let dz = player_pos.z - f64::from(pos.0.z) - 0.5;
327 let distance_sq = dx * dx + dy * dy + dz * dz;
328 distance_sq > MAX_EDIT_DISTANCE * MAX_EDIT_DISTANCE
329 });
330
331 if should_clear {
332 let mut sign = self.sign.lock();
333 if sign.player_who_may_edit == Some(editor_uuid) {
334 sign.player_who_may_edit = None;
335 }
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use std::{array, io::Cursor, sync::Arc};
343
344 use simdnbt::borrow::read_tag;
345 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
346 use steel_registry::{init_vanilla_registry, vanilla_blocks};
347 use steel_utils::BlockPos;
348 use text_components::{Modifier as _, TextComponent};
349 use uuid::Uuid;
350
351 use super::{SignBlockEntity, SignText};
352 use crate::block_entity::BlockEntity as _;
353 use crate::test_support::fresh_test_world;
354
355 #[test]
356 fn plain_sign_lines_save_as_a_string_list() {
357 let mut text = SignText::new();
358 text.messages = array::from_fn(|index| TextComponent::plain(index.to_string()));
359
360 let mut nbt = NbtCompound::new();
361 text.save(&mut nbt);
362
363 assert_eq!(
364 nbt.get("messages"),
365 Some(&NbtTag::List(NbtList::String(vec![
366 "0".into(),
367 "1".into(),
368 "2".into(),
369 "3".into(),
370 ])))
371 );
372 }
373
374 #[test]
375 fn mixed_sign_lines_round_trip_through_the_component_codec() {
376 let mut expected = SignText::new();
377 expected.messages[0] = TextComponent::plain("plain");
378 expected.messages[1] = TextComponent::plain("styled").bold(true);
379
380 let mut nbt = NbtCompound::new();
381 expected.save(&mut nbt);
382 assert!(matches!(
383 nbt.get("messages"),
384 Some(NbtTag::List(NbtList::Compound(_)))
385 ));
386
387 let mut bytes = Vec::new();
388 NbtTag::Compound(nbt).write(&mut bytes);
389 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice()))
390 .expect("saved sign text should be valid NBT");
391 let borrowed_tag = borrowed.as_tag();
392 let compound = borrowed_tag
393 .compound()
394 .expect("saved sign text should be a compound");
395
396 let mut decoded = SignText::new();
397 decoded.load(compound);
398
399 assert_eq!(decoded.messages, expected.messages);
400 assert_eq!(decoded.color, expected.color);
401 assert_eq!(decoded.has_glowing_text, expected.has_glowing_text);
402 }
403
404 #[test]
405 fn sign_tick_releases_state_before_player_lookup_and_editor_clear() {
406 init_vanilla_registry();
407 let world = fresh_test_world("sign_editor_clear");
408 let sign = SignBlockEntity::new(
409 Arc::downgrade(&world),
410 BlockPos::new(8, 64, 8),
411 vanilla_blocks::OAK_SIGN.default_state(),
412 );
413 sign.set_player_who_may_edit(Some(Uuid::from_u128(1)));
414
415 sign.tick(&world);
416 assert_eq!(sign.get_player_who_may_edit(), None);
417 }
418}