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
235 pub fn set_text(&self, text: SignText, front: bool) {
237 let mut sign = self.sign.lock();
238 if front {
239 sign.front_text = text;
240 } else {
241 sign.back_text = text;
242 }
243 }
244}
245
246impl BlockEntity for SignBlockEntity {
247 fn base(&self) -> &BlockEntityBase {
248 &self.base
249 }
250
251 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
252 let nbt_view: BorrowedNbtCompoundView<'_, '_> = nbt.into();
254 let mut sign = self.sign.lock();
255
256 if let Some(front_nbt) = nbt_view.compound("front_text") {
258 sign.front_text.load(front_nbt);
259 }
260
261 if let Some(back_nbt) = nbt_view.compound("back_text") {
263 sign.back_text.load(back_nbt);
264 }
265
266 if let Some(waxed) = nbt_view.byte("is_waxed") {
268 sign.is_waxed = waxed != 0;
269 }
270 }
271
272 fn save_additional(&self, nbt: &mut NbtCompound) {
273 let sign = self.sign.lock();
274 let mut front_nbt = NbtCompound::new();
276 sign.front_text.save(&mut front_nbt);
277 nbt.insert("front_text", front_nbt);
278
279 let mut back_nbt = NbtCompound::new();
281 sign.back_text.save(&mut back_nbt);
282 nbt.insert("back_text", back_nbt);
283
284 nbt.insert("is_waxed", i8::from(sign.is_waxed));
286 }
287
288 fn get_update_tag(&self) -> Option<NbtCompound> {
289 let mut nbt = NbtCompound::new();
291 self.save_additional(&mut nbt);
292 Some(nbt)
293 }
294
295 fn tick(&self, world: &Arc<World>) {
296 let editor_uuid = self.sign.lock().player_who_may_edit;
298 let Some(editor_uuid) = editor_uuid else {
299 return;
300 };
301 let should_clear = world
302 .players
303 .get_by_uuid(&editor_uuid)
304 .is_none_or(|player| {
305 let pos = self.get_block_pos();
306 let player_pos = player.position();
307 let dx = player_pos.x - f64::from(pos.0.x) - 0.5;
308 let dy = player_pos.y - f64::from(pos.0.y) - 0.5;
309 let dz = player_pos.z - f64::from(pos.0.z) - 0.5;
310 let distance_sq = dx * dx + dy * dy + dz * dz;
311 distance_sq > MAX_EDIT_DISTANCE * MAX_EDIT_DISTANCE
312 });
313
314 if should_clear {
315 let mut sign = self.sign.lock();
316 if sign.player_who_may_edit == Some(editor_uuid) {
317 sign.player_who_may_edit = None;
318 }
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use std::{array, io::Cursor, sync::Arc};
326
327 use simdnbt::borrow::read_tag;
328 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
329 use steel_registry::{init_vanilla_registry, vanilla_blocks};
330 use steel_utils::BlockPos;
331 use text_components::{Modifier as _, TextComponent};
332 use uuid::Uuid;
333
334 use super::{SignBlockEntity, SignText};
335 use crate::block_entity::BlockEntity as _;
336 use crate::test_support::fresh_test_world;
337
338 #[test]
339 fn plain_sign_lines_save_as_a_string_list() {
340 let mut text = SignText::new();
341 text.messages = array::from_fn(|index| TextComponent::plain(index.to_string()));
342
343 let mut nbt = NbtCompound::new();
344 text.save(&mut nbt);
345
346 assert_eq!(
347 nbt.get("messages"),
348 Some(&NbtTag::List(NbtList::String(vec![
349 "0".into(),
350 "1".into(),
351 "2".into(),
352 "3".into(),
353 ])))
354 );
355 }
356
357 #[test]
358 fn mixed_sign_lines_round_trip_through_the_component_codec() {
359 let mut expected = SignText::new();
360 expected.messages[0] = TextComponent::plain("plain");
361 expected.messages[1] = TextComponent::plain("styled").bold(true);
362
363 let mut nbt = NbtCompound::new();
364 expected.save(&mut nbt);
365 assert!(matches!(
366 nbt.get("messages"),
367 Some(NbtTag::List(NbtList::Compound(_)))
368 ));
369
370 let mut bytes = Vec::new();
371 NbtTag::Compound(nbt).write(&mut bytes);
372 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice()))
373 .expect("saved sign text should be valid NBT");
374 let borrowed_tag = borrowed.as_tag();
375 let compound = borrowed_tag
376 .compound()
377 .expect("saved sign text should be a compound");
378
379 let mut decoded = SignText::new();
380 decoded.load(compound);
381
382 assert_eq!(decoded.messages, expected.messages);
383 assert_eq!(decoded.color, expected.color);
384 assert_eq!(decoded.has_glowing_text, expected.has_glowing_text);
385 }
386
387 #[test]
388 fn sign_tick_releases_state_before_player_lookup_and_editor_clear() {
389 init_vanilla_registry();
390 let world = fresh_test_world("sign_editor_clear");
391 let sign = SignBlockEntity::new(
392 Arc::downgrade(&world),
393 BlockPos::new(8, 64, 8),
394 vanilla_blocks::OAK_SIGN.default_state(),
395 );
396 sign.set_player_who_may_edit(Some(Uuid::from_u128(1)));
397
398 sign.tick(&world);
399 assert_eq!(sign.get_player_who_may_edit(), None);
400 }
401}