Gododin Gododin Documentation

Linked repository source

examples/ripple_pond/declarations/ripple_pond.odin

This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.

Download the original file · Return to the guide

// Package declarations is the authored native class package the adoption
// tutorial adds to the game: one small native Node2D, RipplePond, that owns
// the pond simulation and hands Godot one packed RGBA8 frame per step.
//
// This is the single source for the class. The struct owns the real instance
// state, the unmarked lifecycle hooks own acquisition and release, and the
// @(gododin) procedures are the admitted Godot methods with real bodies —
// everything else (catalog, bridge, registration, dispatch, wire conversion)
// is generated from this package's compiler graph.
//
// The seam stays coarse by design: one splash call per click, one step call
// and one frame call per rendered frame. GDScript keeps the scene, input,
// UI, texture update, and the preserved GDScript baseline arm.
package declarations

import "core:mem"

import pond "../subsystem"
import gd "gododin:bindings/typed/call"

// Vec2 is the authored two-field form of the engine's single-precision
// Vector2 (field names are free; order and kinds are the contract).
Vec2 :: struct {
	x: f32,
	y: f32,
}

// Splash_Point is the signal payload: the cell the impulse landed in and its
// strength, emitted together and retained by no one.
Splash_Point :: struct {
	at:       Vec2,
	strength: f64,
}

// Ripple_Pond is the game's native node. Godot owns settings presentation
// (the saved damping property), input, the Image/ImageTexture, and scene
// orchestration; this node owns the pond workspace and the simulation.
@(gododin_class="Node2D")
Ripple_Pond :: struct {
	// The saved native setting: an Inspector-visible f64 the engine stores
	// in the scene and restores across editor reloads and game launches.
	damping: f64 `godot:"property"`,
	// Emitted on every accepted splash with the cell it landed in. The name
	// must differ from the splash method's Godot-visible name — ClassDB
	// refuses one class carrying both as the same name.
	splashed: gd.Signal(Splash_Point),
	// The Host-provided owning allocator, recorded once at admission so the
	// lazy workspace allocation in configure stays allocator-symmetric with
	// deinit.
	allocator:    mem.Allocator,
	pond:         ^pond.Pond,
	last_error:   i64,
	splash_count: i64,
}

// Named result vocabulary through get_last_error:
//
//	1 — the method ran before configure accepted a valid shape
//	2 — configure refused the dimensions
ripple_pond_init :: proc(self: ^Ripple_Pond, allocator: mem.Allocator) -> bool {
	self.allocator = allocator
	self.damping = 0.994
	self.last_error = 0
	self.splash_count = 0
	return true
}

ripple_pond_deinit :: proc(self: ^Ripple_Pond) {
	if self.pond != nil {
		_ = pond.pond_destroy(&self.pond)
	}
}

// configure shapes the pond to the given cell rectangle and resets it to
// flat water. The workspace allocates once, lazily, inside the instance's
// recorded owning allocator; configure itself never allocates afterwards.
@(gododin)
ripple_pond_configure :: proc(self: ^Ripple_Pond, width, height: i64) -> i64 {
	if width < 8 || height < 8 || width > pond.MAX_DIMENSION || height > pond.MAX_DIMENSION {
		self.last_error = 2
		return -1
	}
	if self.pond == nil {
		created := pond.pond_create(self.allocator)
		if created == nil {
			self.last_error = 1
			return -1
		}
		self.pond = created
	}
	if !pond.pond_configure(self.pond, i32(width), i32(height), self.damping) {
		self.last_error = 2
		return -1
	}
	self.last_error = 0
	return 0
}

// splash adds an impulse at the given cell and emits the splash signal with
// the position it landed in. The cell is clamped to the interior, so any
// in-rectangle coordinate is accepted; a splash before configure refuses.
@(gododin)
ripple_pond_splash :: proc(self: ^Ripple_Pond, call: gd.Main_Call, x, y: i64, strength: f64) -> i64 {
	if self.pond == nil {
		self.last_error = 1
		return -1
	}
	pond.pond_splash(self.pond, i32(x), i32(y), strength)
	self.splash_count += 1
	emit_error := gd.emit(call, self.splashed, Splash_Point{
		at       = Vec2{x = f32(x), y = f32(y)},
		strength = strength,
	})
	if emit_error != .None && emit_error != .Godot_Call_Failed {
		self.last_error = 1
		return -1
	}
	self.last_error = 0
	return self.splash_count
}

// step advances the simulation by one fixed damped wave-equation step and
// returns the total step count. No engine calls happen in the hot path.
@(gododin)
ripple_pond_step :: proc(self: ^Ripple_Pond) -> i64 {
	if self.pond == nil {
		self.last_error = 1
		return -1
	}
	pond.pond_step(self.pond)
	self.last_error = 0
	return self.pond.steps
}

// frame_bytes paints the current heights into the retained RGBA8 frame and
// returns the borrowed bytes, valid until the next frame call on this
// instance; the Host copies them into the engine's PackedByteArray exactly
// once. One coarse call per frame — no per-cell foreign calls.
@(gododin)
ripple_pond_frame_bytes :: proc(self: ^Ripple_Pond) -> []u8 {
	if self.pond == nil {
		self.last_error = 1
		return nil
	}
	return pond.pond_frame(self.pond)
}

@(gododin)
ripple_pond_get_steps :: proc(self: ^Ripple_Pond) -> i64 {
	if self.pond == nil do return 0
	return self.pond.steps
}

@(gododin)
ripple_pond_get_splash_count :: proc(self: ^Ripple_Pond) -> i64 {
	return self.splash_count
}

@(gododin)
ripple_pond_get_last_error :: proc(self: ^Ripple_Pond) -> i64 {
	return self.last_error
}