| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Practical Example | Practical Example |
| Reference | Reference |
Script utility modules are the "batteries" a game needs beyond nodes and the runtime API: reading and writing save files, converting data to and from JSON, deterministic random and hashing, gameplay math, logging, and networking. They live under perro_api::modules (plus perro_api::networking) and are available in any script or shared project module without adding a dependency.
- Persist a save game or settings file to disk (or browser storage on web):
modules::file::save_string/load_stringwith auser://path, serialized throughmodules::json::stringify/parse. - Deterministic procedural generation so one seed always builds the same dungeon, loot table, or enemy wave:
modules::random::hash_str,rand_range_i32,SeededRng, and the*_streamhelpers. - Smooth camera follow, AI turning, and value easing without hand-rolling the math:
modules::math::smooth_damp,damp,approach,lerp_angle_deg,wrap_angle_deg. - Online features β a leaderboard fetch or a lobby socket:
perro_api::networkingHttpClient/NetworkWorld, polled inon_updateand bridged to signals withemit_http_event!/emit_net_event!. - Debug output while iterating on game logic:
log_info!,log_warn!,log_error!.
Use these modules for stateless work or external services. Put reusable game rules in a project module, and put mutable per-instance values in #[State]. Do not hide node ownership or long-lived mutable game state in a utility module; doing so makes scene wiring and script reload behavior unclear.
Roll three loot ids deterministically from the run seed and write them to a per-user save file as JSON, so a reloaded run drops the same items.
use perro_api::{modules::{file, json, random::SeededRng}, prelude::*};
fn save_run_loot(run_seed: u32) -> std::io::Result<()> {
let mut rng = SeededRng::new(run_seed);
let loot: Vec<i32> = (0..3).map(|_| rng.next_range_i32(0, 20)).collect();
let payload = json::stringify(&loot.to_variant()).unwrap_or_default();
file::save_string("user://runs/last_loot.json", &payload)
}Perro exposes utility modules through perro_api::modules.
Import from prelude:
use perro_api::prelude::*;Or import specific module:
use perro_api::modules::random;File IO helpers backed by project path resolver.
set_project_root_disk(root: &str, name: &str)load_bytes(path: impl ResPathSource) -> io::Result<Vec<u8>>load_string(path: impl ResPathSource) -> io::Result<String>save_bytes(path: impl ResPathSource, data: &[u8]) -> io::Result<()>save_string(path: impl ResPathSource, data: &str) -> io::Result<()>exists(path: impl ResPathSource) -> boolresolve_path_string(path: impl ResPathSource) -> String
Write restriction:
- write paths must be
user://...or absolute paths - relative non-
user://writes return permission error
Web target note:
user://...load/save use browserlocalStorage- key fmt:
perro:user:<ProjectName>:data:<relative_user_path> - vals use base64 so
save_byteswork for binary data too sessionStorage+ cookie save path ! usemodules::file- use
perro_web::storage::*in runtime Rust code for session/cookie vals
JSON <-> Variant conversion helpers.
parse(json_str: &str) -> Result<Variant, serde_json::Error>stringify(value: &Variant) -> Result<String, serde_json::Error>
Log helpers + macros.
Functions:
print(message: impl Display)info(message: impl Display)warn(message: impl Display)error(message: impl Display)
Macros:
log_print!(...)log_info!(...)log_warn!(...)log_error!(...)
Networking API.
Lives in perro_networking.
Re-exported by perro_api::networking and perro_api::prelude::*.
Includes thin TCP/UDP helpers over std::net.
This is not a node API. Keep sockets in script state, poll them during update, then emit signals if desired.
Types:
HttpIDHttpClientHttpConfigHttpHeadersHttpTLSModeHttpProxyHttpRequestHttpResponseHttpEventHttpMethodHttpBodyHttpErrorHttpErrorKindNetworkWorldNetworkEventNetSourceTcpHostTcpConnectionTcpHostIdTcpConnectionIdUdpEndpointUdpEndpointIdUdpPacketNetEventNetHandshakeNetErrorNetErrorKind
HTTP:
- full guide:
../networking/http.md HttpClient::new()HttpClient::with_config(config)request(HttpRequest) -> HttpIDget(url) -> HttpIDpost_variant(url, Variant) -> HttpIDpoll() -> Option<HttpEvent>poll_all(max_events) -> Vec<HttpEvent>emit_http_event!(ctx, event) -> usize
Network world:
NetworkWorld::new() -> NetworkWorldbind_tcp_host(addr) -> NetResult<TcpHostId>connect_tcp(addr) -> NetResult<TcpConnectionId>bind_udp(addr) -> NetResult<UdpEndpointId>tcp_host_addr(id) -> NetResult<SocketAddr>tcp_peer_addr(id) -> NetResult<SocketAddr>udp_addr(id) -> NetResult<SocketAddr>tcp_send(id, bytes) -> NetResult<usize>tcp_send_frame(id, bytes) -> NetResult<()>tcp_send_handshake(id, handshake) -> NetResult<()>tcp_send_heartbeat_ping(id) -> NetResult<()>tcp_send_heartbeat_pong(id) -> NetResult<()>udp_send_to(id, bytes, addr) -> NetResult<usize>poll_events(max_per_socket, max_bytes) -> Vec<NetworkEvent>poll_frame_events(max_per_socket, max_frame_bytes) -> Vec<NetworkEvent>- remove:
remove_tcp_host,remove_tcp_connection,remove_udp
TCP host:
TcpHost::bind(addr) -> NetResult<TcpHost>local_addr() -> SocketAddraccept() -> NetResult<Option<TcpConnection>>accept_event() -> NetResult<Option<(TcpConnection, NetEvent)>>
TCP connection:
TcpConnection::connect(addr) -> NetResult<TcpConnection>TcpConnection::from_stream(stream) -> NetResult<TcpConnection>peer_addr() -> SocketAddrpeer_string() -> Stringconnected_event() -> NetEventread_available(max_bytes) -> NetResult<Option<Vec<u8>>>poll_event(max_bytes) -> NetResult<Option<NetEvent>>write(bytes) -> NetResult<usize>write_all(bytes) -> NetResult<()>write_frame(bytes) -> NetResult<()>write_handshake(handshake) -> NetResult<()>poll_frame(max_frame_bytes) -> NetResult<Option<Vec<u8>>>poll_frame_event(max_frame_bytes) -> NetResult<Option<NetEvent>>poll_handshake(max_frame_bytes) -> NetResult<Option<NetHandshake>>
UDP endpoint:
UdpEndpoint::bind(addr) -> NetResult<UdpEndpoint>local_addr() -> SocketAddrsend_to(bytes, addr) -> NetResult<usize>recv_from(max_bytes) -> NetResult<Option<UdpPacket>>poll_event(max_bytes) -> NetResult<Option<NetEvent>>
NetEvent signal bridge:
signal_name() -> &'static strsignal_id() -> SignalIDsignal_params() -> Vec<Variant>
Macro:
emit_net_event!(ctx, event) -> usize
Frame helpers:
encode_frame(bytes) -> NetResult<Vec<u8>>decode_next_frame(buffer, max_frame_bytes) -> NetResult<Option<Vec<u8>>>
Handshake:
NetHandshake::new(app, protocol, version)encode() -> NetResult<Vec<u8>>decode(bytes) -> NetResult<NetHandshake>validate(expected) -> NetResult<()>
Heartbeat:
heartbeat_ping() -> &'static [u8]heartbeat_pong() -> &'static [u8]is_heartbeat_ping(bytes) -> boolis_heartbeat_pong(bytes) -> bool
Signal names:
TCP_ConnectedTCP_ClientConnectedTCP_DataTCP_DisconnectedUDP_PacketTCP_FrameNet_HeartbeatPingNet_HeartbeatPongNet_Error
Notes:
- accept/read/recv use non-blocking sockets
TcpConnection::connectis sync std connect, so avoid calling it on hot frame pathpoll_eventreturnsNonewhen no data is ready- use raw poll or frame poll, not both on same TCP connection
Math helpers:
deg_to_rad(degrees: f32) -> f32rad_to_deg(radians: f32) -> f32clamp01(value: f32) -> f32lerp(start: f32, end: f32, t: f32) -> f32ilerp(start: f32, end: f32, value: f32) -> f32slerp(start: f32, end: f32, t: f32) -> f32islerp(start: f32, end: f32, value: f32) -> f32remap(in_min: f32, in_max: f32, out_min: f32, out_max: f32, value: f32) -> f32smoothstep(edge0: f32, edge1: f32, value: f32) -> f32ismoothstep(edge0: f32, edge1: f32, value: f32) -> f32angle_diff_rad(from: f32, to: f32) -> f32angle_diff_deg(from: f32, to: f32) -> f32lerp_angle_rad(from: f32, to: f32, t: f32) -> f32lerp_angle_deg(from: f32, to: f32, t: f32) -> f32wrap_angle_rad(angle: f32) -> f32in[-PI, PI)wrap_angle_deg(angle: f32) -> f32in[-180, 180)approach(current: f32, target: f32, max_delta: f32) -> f32damp(current: f32, target: f32, lambda: f32, delta_time: f32) -> f32smooth_damp(current, target, current_velocity, smooth_time, max_speed, delta_time) -> (f32, f32)repeat(value: f32, length: f32) -> f32ping_pong(value: f32, length: f32) -> f32nearly_eq(a: f32, b: f32, epsilon: f32) -> bool- macros:
deg_to_rad!(x),rad_to_deg!(x)
Deterministic helpers for seeded random generation and stable hashing.
hash<T: HashToU32>(value: T) -> u32- trait:
HashToU32(u32,i32,u64,i64,u128,bool,f32) hash_u32(value: u32) -> u32hash_i32(value: i32) -> u32hash_u64(value: u64) -> u32hash_i64(value: i64) -> u32hash_u128(value: u128) -> u32hash_bool(value: bool) -> u32hash_f32(value: f32) -> u32hash_bytes(bytes: &[u8]) -> u32hash_str(value: &str) -> u32hash_combine(a: u32, b: u32) -> u32hash_combine3(a: u32, b: u32, c: u32) -> u32hash_combine4(a: u32, b: u32, c: u32, d: u32) -> u32hash2_u32(x: u32, y: u32) -> u32hash3_u32(x: u32, y: u32, z: u32) -> u32
hash64_u32(value: u32) -> u64hash64_u64(value: u64) -> u64hash64_u128(value: u128) -> u64hash64_bytes(bytes: &[u8]) -> u64hash64_str(value: &str) -> u64
rand_range<T: RandRangeValue>(min: T, max: T, seed: u32) -> T- trait:
RandRangeValue(f32,i32,u32) rand_u32(seed: u32) -> u32rand01(seed: u32) -> f32in[0, 1]rand11(seed: u32) -> f32in[-1, 1]rand_range_f32(min: f32, max: f32, seed: u32) -> f32rand_range_i32(min: i32, max: i32, seed: u32) -> i32rand_range_u32(min: u32, max: u32, seed: u32) -> u32chance(probability: f32, seed: u32) -> boolchoose_index(len: usize, seed: u32) -> Option<usize>
rand_u32_stream(seed: u32, index: u32) -> u32rand01_stream(seed: u32, index: u32) -> f32in[0, 1]rand11_stream(seed: u32, index: u32) -> f32in[-1, 1]rand_unit_vec2(seed: u32) -> (f32, f32)rand_unit_vec3(seed: u32) -> (f32, f32, f32)rand_in_circle(seed: u32) -> (f32, f32)shuffle(seed: u32, values: &mut [T])
Use stream helpers when you need multiple stable random values from one base seed.
SeededRng gives deterministic sequence with internal state:
SeededRng::new(seed: u32) -> SeededRngseed(&self) -> u32reseed(&mut self, seed: u32)next_u32(&mut self) -> u32next_01(&mut self) -> f32in[0, 1]next_11(&mut self) -> f32in[-1, 1]next_range<T: RandRangeValue>(&mut self, min: T, max: T) -> Tnext_range_f32(&mut self, min: f32, max: f32) -> f32next_range_i32(&mut self, min: i32, max: i32) -> i32next_range_u32(&mut self, min: u32, max: u32) -> u32next_chance(&mut self, probability: f32) -> boolnext_index(&mut self, len: usize) -> Option<usize>
Example:
use perro_api::prelude::*;
let base_seed = hash_str("enemy_wave_01");
let jitter = rand11_stream(base_seed, 0);
let speed_scale = 0.8 + rand01_stream(base_seed, 1) * 0.4;
let mut rng = SeededRng::new(base_seed);
let color_pick = rng.next_u32() % 4;