pumpkinplus/modules/mechanics/world/
openable.rs1use crate::config::ConfigManager;
15use crate::mechanics::mechanic::Mechanic;
16use crate::{GameMode, InteractAction};
17use pumpkin_plugin_api::events::{EventData, EventHandler, EventPriority, PlayerInteractEvent};
18use pumpkin_plugin_api::world::{BlockFlags, BlockPos, World};
19use pumpkin_plugin_api::{Context, Server};
20use serde::{Deserialize, Serialize};
21use tracing::{debug, info};
22
23#[derive(Default)]
25pub struct Openable;
26
27impl Mechanic for Openable {
28 fn enabled(&self) -> bool {
29 ConfigManager::get()
30 .map(|cm| cm.get_config::<OpenableConfig>().enabled)
31 .unwrap_or(true)
32 }
33
34 fn events(&self, context: &Context) {
35 context
36 .register_event_handler::<PlayerInteractEvent, _>(Openable, EventPriority::Normal, true)
37 .expect("failed to register openable event handler");
38 }
39}
40
41impl EventHandler<PlayerInteractEvent> for Openable {
42 fn handle(
43 &self,
44 _server: Server,
45 mut event: EventData<PlayerInteractEvent>,
46 ) -> EventData<PlayerInteractEvent> {
47 info!("[Openable] handle triggered");
48
49 if !self.enabled() {
50 info!("[Openable] module disabled, returning early");
51 return event;
52 }
53
54 let config: OpenableConfig = ConfigManager::get()
55 .map(|cm| cm.get_config())
56 .unwrap_or_default();
57
58 let action = InteractAction::from(event.action);
59 info!(
60 "[Openable] raw action debug = {:?}, parsed action = {:?}, config.actions = {:?}",
61 event.action, action, config.actions
62 );
63 if !action.matches_config(&config.actions) {
64 info!("[Openable] action does not match config, returning early");
65 return event;
66 }
67
68 let gamemode = GameMode::from(event.player.get_gamemode());
69 info!(
70 "[Openable] gamemode = {:?}, config.gamemodes = {:?}",
71 gamemode, config.gamemodes
72 );
73 if !gamemode.matches_config(&config.gamemodes) {
74 info!("[Openable] gamemode does not match config, returning early");
75 return event;
76 }
77
78 info!("[Openable] block = {}", event.block);
79 if !event.block.ends_with("_door") {
80 info!("[Openable] block is not a door, returning early");
81 return event;
82 }
83
84 let Some(clicked_pos) = event.clicked_pos else {
85 info!("[Openable] no clicked_pos, returning early");
86 return event;
87 };
88 info!("[Openable] clicked_pos = {:?}", clicked_pos);
89
90 let world = event.player.get_world();
91
92 let clicked_state_id = world.get_block_state_id(clicked_pos);
93 info!("[Openable] clicked_state_id = {}", clicked_state_id);
94
95 let adjacent_pos = find_adjacent_door(&world, clicked_pos);
96
97 let Some(adjacent_pos) = adjacent_pos else {
98 info!("[Openable] no adjacent door found, returning early");
99 return event;
100 };
101 info!("[Openable] adjacent_pos = {:?}", adjacent_pos);
102
103 let adjacent_state_id = world.get_block_state_id(adjacent_pos);
104 info!("[Openable] adjacent_state_id = {}", adjacent_state_id);
105
106 if clicked_state_id == adjacent_state_id {
107 info!("[Openable] clicked_state_id == adjacent_state_id, returning early");
108 return event;
109 }
110
111 let toggled_clicked_id = find_toggled_door_state(clicked_state_id);
112 let toggled_adjacent_id = find_toggled_door_state(adjacent_state_id);
113 info!(
114 "[Openable] toggled_clicked_id = {:?}, toggled_adjacent_id = {:?}",
115 toggled_clicked_id, toggled_adjacent_id
116 );
117
118 if let (Some(new_clicked), Some(new_adjacent)) = (toggled_clicked_id, toggled_adjacent_id) {
119 event.cancelled = true;
120 info!("[Openable] event cancelled, syncing door states");
121
122 let flags = BlockFlags::NOTIFY_NEIGHBORS | BlockFlags::NOTIFY_LISTENERS;
123
124 world.set_block_state(clicked_pos, new_clicked, flags);
125 world.set_block_state(adjacent_pos, new_adjacent, flags);
126
127 debug!(
128 "Synced double doors at {:?} and {:?} (states {} -> {}, {} -> {})",
129 clicked_pos,
130 adjacent_pos,
131 clicked_state_id,
132 new_clicked,
133 adjacent_state_id,
134 new_adjacent
135 );
136 }
137
138 event
139 }
140}
141
142fn find_adjacent_door(world: &World, pos: BlockPos) -> Option<BlockPos> {
149 let neighbors = [
150 BlockPos {
151 x: pos.x + 1,
152 y: pos.y,
153 z: pos.z,
154 },
155 BlockPos {
156 x: pos.x - 1,
157 y: pos.y,
158 z: pos.z,
159 },
160 BlockPos {
161 x: pos.x,
162 y: pos.y,
163 z: pos.z + 1,
164 },
165 BlockPos {
166 x: pos.x,
167 y: pos.y,
168 z: pos.z - 1,
169 },
170 ];
171
172 for neighbor in &neighbors {
173 let state = world.get_block_state(*neighbor);
174 if !state.is_air && !state.is_liquid {
175 return Some(*neighbor);
176 }
177 }
178
179 None
180}
181
182fn find_toggled_door_state(state_id: u16) -> Option<u16> {
193 let candidates = [
194 state_id.wrapping_add(1),
195 state_id.wrapping_sub(1),
196 state_id.wrapping_add(2),
197 state_id.wrapping_sub(2),
198 state_id.wrapping_add(4),
199 state_id.wrapping_sub(4),
200 ];
201 Some(candidates[0])
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct OpenableConfig {
207 pub enabled: bool,
209 pub gamemodes: Vec<GameMode>,
211 pub actions: Vec<InteractAction>,
213}
214
215impl Default for OpenableConfig {
216 fn default() -> Self {
217 Self {
218 enabled: false,
219 gamemodes: vec![GameMode::Survival, GameMode::Adventure],
220 actions: vec![InteractAction::RightClickBlock],
221 }
222 }
223}