PatchBukkit/rust/src/java/native_callbacks/server.rs
2026-09-18 20:19:58 +02:00

462 lines
16 KiB
Rust

use pumpkin_util::text::TextComponent;
use std::sync::atomic::Ordering;
use crate::{
java::native_callbacks::CALLBACK_CONTEXT,
proto::patchbukkit::{
common::{EmptyRequest, Uuid as ProtoUuid},
server::{
BanEntryProto, BroadcastMessageRequest, CreateWorldRequest, CreateWorldResponse,
GetBanListRequest, GetBanListResponse, GetOperatorsResponse, GetWhitelistResponse,
OperatorEntryProto, ServerInfoResponse, ServerTickInfoResponse, SetBanEntryRequest,
SetOperatorRequest, SetServerDefaultGamemodeRequest, SetServerIdleTimeoutRequest,
SetServerMaxPlayersRequest, SetServerMotdRequest, SetServerTickRateRequest,
SetServerWhitelistEnforcedRequest, SetServerWhitelistRequest,
SetWhitelistPlayerRequest, ShutdownServerRequest, UnloadWorldRequest,
UnloadWorldResponse, WhitelistEntryProto,
},
},
};
pub fn ffi_native_bridge_get_server_info_impl(
_request: EmptyRequest,
) -> Option<ServerInfoResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = &ctx.plugin_context.server;
let java_config = &server.advanced_config.networking.java;
let motd = java_config.motd.clone();
let ip = java_config.address.ip().to_string();
let port = java_config.address.port() as i32;
let max_players = java_config.max_players as i32;
let view_distance = java_config.view_distance.get() as i32;
let simulation_distance = java_config.simulation_distance.get() as i32;
let allow_flight = true;
let allow_nether = server.basic_config.allow_nether;
let allow_end = server.basic_config.allow_end;
let online_mode = java_config.online_mode;
let hardcore = server.basic_config.hardcore;
let pvp = server.advanced_config.pvp.enabled;
let has_whitelist = server.white_list.load(Ordering::Relaxed);
let is_whitelist_enforced = server.basic_config.enforce_whitelist;
let spawn_protection = 16;
let idle_timeout = server.player_idle_timeout.load(Ordering::Relaxed);
let default_gamemode = if let Ok(gm) = server.defaultgamemode.try_lock() {
format!("{:?}", gm.gamemode)
} else {
"SURVIVAL".to_string()
};
Some(ServerInfoResponse {
server_name: "Pumpkin".to_string(),
version: "0.1.0".to_string(),
bukkit_version: format!("{}-R0.1-SNAPSHOT", pumpkin_data::packet::CURRENT_MC_VERSION),
minecraft_version: pumpkin_data::packet::CURRENT_MC_VERSION.to_string(),
motd,
ip,
port,
max_players,
view_distance,
simulation_distance,
allow_flight,
allow_nether,
allow_end,
online_mode,
hardcore,
pvp,
has_whitelist,
is_whitelist_enforced,
spawn_protection,
default_gamemode,
idle_timeout,
generate_structures: true,
max_world_size: 29999984,
})
}
pub fn ffi_native_bridge_set_server_motd_impl(_request: SetServerMotdRequest) -> Option<()> {
Some(())
}
pub fn ffi_native_bridge_set_server_max_players_impl(
_request: SetServerMaxPlayersRequest,
) -> Option<()> {
Some(())
}
pub fn ffi_native_bridge_set_server_whitelist_impl(
request: SetServerWhitelistRequest,
) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
ctx.plugin_context
.server
.white_list
.store(request.enabled, Ordering::Relaxed);
Some(())
}
pub fn ffi_native_bridge_set_server_whitelist_enforced_impl(
_request: SetServerWhitelistEnforcedRequest,
) -> Option<()> {
Some(())
}
pub fn ffi_native_bridge_set_server_idle_timeout_impl(
request: SetServerIdleTimeoutRequest,
) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
ctx.plugin_context
.server
.player_idle_timeout
.store(request.timeout_minutes, Ordering::Relaxed);
Some(())
}
pub fn ffi_native_bridge_set_server_default_gamemode_impl(
request: SetServerDefaultGamemodeRequest,
) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
if let Ok(mut gm) = ctx.plugin_context.server.defaultgamemode.try_lock() {
let mode = match request.gamemode.to_uppercase().as_str() {
"CREATIVE" => pumpkin_util::GameMode::Creative,
"ADVENTURE" => pumpkin_util::GameMode::Adventure,
"SPECTATOR" => pumpkin_util::GameMode::Spectator,
_ => pumpkin_util::GameMode::Survival,
};
gm.gamemode = mode;
}
Some(())
}
pub fn ffi_native_bridge_get_server_tick_info_impl(
_request: EmptyRequest,
) -> Option<ServerTickInfoResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = &ctx.plugin_context.server;
let tick_rate = server.tick_rate_manager.tickrate();
let is_frozen = server.tick_rate_manager.is_frozen();
let is_sprinting = server.tick_rate_manager.is_sprinting();
let is_stepping = server.tick_rate_manager.is_stepping_forward();
let tick_count = server.tick_count.load(Ordering::Relaxed) as i64;
let avg_nanos = server.aggregated_tick_times_nanos.load(Ordering::Relaxed);
let average_tick_time = (avg_nanos as f64) / 1_000_000.0 / 100.0;
let mut tick_times_nanos = Vec::new();
if let Ok(times) = server.tick_times_nanos.try_lock() {
tick_times_nanos.extend_from_slice(&*times);
} else {
tick_times_nanos.resize(100, 50_000_000);
}
let tps_val = (1_000.0 / (average_tick_time.max(50.0))).min(tick_rate as f64);
let tps = vec![tps_val, tps_val, tps_val];
Some(ServerTickInfoResponse {
tps,
tick_rate,
is_frozen,
is_sprinting,
is_stepping,
average_tick_time,
tick_count,
tick_times_nanos,
})
}
pub fn ffi_native_bridge_set_server_tick_rate_impl(
request: SetServerTickRateRequest,
) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = &ctx.plugin_context.server;
if request.tick_rate > 0.0 {
server
.tick_rate_manager
.set_tick_rate(server, request.tick_rate);
}
server.tick_rate_manager.set_frozen(server, request.frozen);
Some(())
}
pub fn ffi_native_bridge_shutdown_server_impl(request: ShutdownServerRequest) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
ctx.runtime.spawn(async move {
if request.save {
let _ = server.save_all().await;
}
server.shutdown().await;
});
Some(())
}
pub fn ffi_native_bridge_broadcast_message_impl(request: BroadcastMessageRequest) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
let text = TextComponent::text(request.message);
let sender = TextComponent::text("Server");
server.broadcast_message(&text, &sender, 0, None);
Some(())
}
pub fn ffi_native_bridge_get_whitelist_impl(
_request: EmptyRequest,
) -> Option<GetWhitelistResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let mut players = Vec::new();
if let Ok(config) = ctx.plugin_context.server.data.whitelist_config.try_read() {
for entry in &config.whitelist {
players.push(WhitelistEntryProto {
uuid: Some(ProtoUuid {
value: entry.uuid.to_string(),
}),
name: entry.name.clone(),
});
}
}
Some(GetWhitelistResponse { players })
}
pub fn ffi_native_bridge_set_whitelist_player_impl(
request: SetWhitelistPlayerRequest,
) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let uuid_str = &request.uuid.as_ref()?.value;
let target_uuid = uuid::Uuid::parse_str(uuid_str).ok()?;
let target_name = request.name;
let server = ctx.plugin_context.server.clone();
let mut config = server
.data
.whitelist_config
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if request.whitelisted {
if !config.whitelist.iter().any(|e| e.uuid == target_uuid) {
config
.whitelist
.push(pumpkin_config::whitelist::WhitelistEntry {
uuid: target_uuid,
name: target_name,
});
}
} else {
config.whitelist.retain(|e| e.uuid != target_uuid);
}
Some(())
}
pub fn ffi_native_bridge_get_operators_impl(
_request: EmptyRequest,
) -> Option<GetOperatorsResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let mut operators = Vec::new();
if let Ok(config) = ctx.plugin_context.server.data.operator_config.try_read() {
for op in &config.ops {
operators.push(OperatorEntryProto {
uuid: Some(ProtoUuid {
value: op.uuid.to_string(),
}),
name: op.name.clone(),
level: op.level as i32,
bypasses_player_limit: op.bypasses_player_limit,
});
}
}
Some(GetOperatorsResponse { operators })
}
pub fn ffi_native_bridge_set_operator_impl(request: SetOperatorRequest) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let uuid_str = &request.uuid.as_ref()?.value;
let target_uuid = uuid::Uuid::parse_str(uuid_str).ok()?;
let target_name = request.name;
let level = match request.level {
1 => pumpkin_util::permission::PermissionLvl::One,
2 => pumpkin_util::permission::PermissionLvl::Two,
3 => pumpkin_util::permission::PermissionLvl::Three,
_ => pumpkin_util::permission::PermissionLvl::Four,
};
let server = ctx.plugin_context.server.clone();
let mut config = server
.data
.operator_config
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if request.is_op {
if let Some(op) = config.ops.iter_mut().find(|e| e.uuid == target_uuid) {
op.level = level;
} else {
config.ops.push(pumpkin_config::op::Op {
uuid: target_uuid,
name: target_name,
level,
bypasses_player_limit: false,
});
}
} else {
config.ops.retain(|e| e.uuid != target_uuid);
}
Some(())
}
pub fn ffi_native_bridge_get_ban_list_impl(
request: GetBanListRequest,
) -> Option<GetBanListResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let mut entries = Vec::new();
if request.ban_type == "IP" {
if let Ok(list) = ctx.plugin_context.server.data.banned_ip_list.try_read() {
for entry in &list.banned_ips {
let created = entry.created.unix_timestamp();
let expires = entry
.expires
.map_or(0, time::OffsetDateTime::unix_timestamp);
entries.push(BanEntryProto {
target: entry.ip.to_string(),
source: entry.source.clone(),
created,
expires,
reason: entry.reason.clone(),
});
}
}
} else if let Ok(list) = ctx.plugin_context.server.data.banned_player_list.try_read() {
for entry in &list.banned_players {
let created = entry.created.unix_timestamp();
let expires = entry
.expires
.map_or(0, time::OffsetDateTime::unix_timestamp);
entries.push(BanEntryProto {
target: entry.name.clone(),
source: entry.source.clone(),
created,
expires,
reason: entry.reason.clone(),
});
}
}
Some(GetBanListResponse { entries })
}
pub fn ffi_native_bridge_set_ban_entry_impl(request: SetBanEntryRequest) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
if let Some(entry) = request.entry {
if request.ban_type == "IP" {
if let Ok(ip) = entry.target.parse::<std::net::IpAddr>() {
let mut list = server
.data
.banned_ip_list
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if request.remove {
list.banned_ips.retain(|e| e.ip != ip);
} else {
list.banned_ips.retain(|e| e.ip != ip);
let expires = if entry.expires > 0 {
time::OffsetDateTime::from_unix_timestamp(entry.expires).ok()
} else {
None
};
list.banned_ips
.push(pumpkin::data::banlist_serializer::BannedIpEntry {
ip,
created: time::OffsetDateTime::now_utc(),
source: entry.source,
expires,
reason: entry.reason,
});
}
}
} else {
let mut list = server
.data
.banned_player_list
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let target_name = entry.target.clone();
if request.remove {
list.banned_players
.retain(|e| !e.name.eq_ignore_ascii_case(&target_name));
} else {
list.banned_players
.retain(|e| !e.name.eq_ignore_ascii_case(&target_name));
let expires = if entry.expires > 0 {
time::OffsetDateTime::from_unix_timestamp(entry.expires).ok()
} else {
None
};
list.banned_players
.push(pumpkin::data::banlist_serializer::BannedPlayerEntry {
uuid: uuid::Uuid::nil(),
name: target_name,
created: time::OffsetDateTime::now_utc(),
source: entry.source,
expires,
reason: entry.reason,
});
}
}
}
Some(())
}
pub fn ffi_native_bridge_create_world_impl(
request: CreateWorldRequest,
) -> Option<CreateWorldResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
let name = request.name;
let dim = match request.dimension.as_str() {
"minecraft:the_nether" => pumpkin_data::dimension::Dimension::THE_NETHER,
"minecraft:the_end" => pumpkin_data::dimension::Dimension::THE_END,
_ => pumpkin_data::dimension::Dimension::OVERWORLD,
};
let world_name = name.clone();
let (tx, rx) = tokio::sync::oneshot::channel();
ctx.runtime.spawn(async move {
let world = server.create_world(world_name, dim);
let _ = tx.send(world.uuid);
});
let world_uuid = rx.blocking_recv().ok()?;
Some(CreateWorldResponse {
world_uuid: Some(ProtoUuid {
value: world_uuid.to_string(),
}),
name,
success: true,
})
}
pub fn ffi_native_bridge_unload_world_impl(
request: UnloadWorldRequest,
) -> Option<UnloadWorldResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
let name = request.world_name;
let (tx, rx) = tokio::sync::oneshot::channel();
ctx.runtime.spawn(async move {
let res = server.unload_world(&name).await;
let _ = tx.send(res.is_ok());
});
let success = rx.blocking_recv().unwrap_or(false);
Some(UnloadWorldResponse { success })
}
pub fn ffi_native_bridge_save_all_worlds_impl(_request: EmptyRequest) -> Option<()> {
let ctx = CALLBACK_CONTEXT.get()?;
let server = ctx.plugin_context.server.clone();
ctx.runtime.spawn(async move {
let _ = server.save_all().await;
});
Some(())
}