// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
suberror FfError {
NotSupported
NoGamepads
Disconnected(GamepadId)
FfNotSupported(GamepadId)
InvalidDistanceModel(DistanceModelError)
}
///|
suberror DistanceModelError {
InvalidReferenceDistance
InvalidRolloffFactor
InvalidMaxDistance
InvalidModelParameter
}
///|
pub struct Ticks {
ms : Int64
}
///|
pub fn Ticks::from_ms(ms : Int64) -> Ticks {
if ms <= 0L {
{ ms: 0L }
} else {
let tick = 50L
let q = (ms + tick - 1L) / tick
{ ms: q * tick }
}
}
///|
pub fn Ticks::as_ms(self : Ticks) -> Int64 {
self.ms
}
///|
pub(all) enum BaseEffectType {
Weak(Int)
Strong(Int)
}
///|
pub struct BaseEffect {
kind : BaseEffectType
scheduling : Replay
envelope : Envelope
}
///|
pub fn BaseEffect::default() -> BaseEffect {
{
kind: Weak(0),
scheduling: Replay::default(),
envelope: Envelope::default(),
}
}
///|
pub enum FfRepeat {
Infinitely
For(Int64)
}
///|
let _ff_repeat_keepalive : Array[FfRepeat] = [For(0L)]
///|
pub enum DistanceModel {
None
Linear(
ref_distance~ : Double,
rolloff_factor~ : Double,
max_distance~ : Double
)
LinearClamped(
ref_distance~ : Double,
rolloff_factor~ : Double,
max_distance~ : Double
)
Inverse(ref_distance~ : Double, rolloff_factor~ : Double)
InverseClamped(
ref_distance~ : Double,
rolloff_factor~ : Double,
max_distance~ : Double
)
Exponential(ref_distance~ : Double, rolloff_factor~ : Double)
ExponentialClamped(
ref_distance~ : Double,
rolloff_factor~ : Double,
max_distance~ : Double
)
}
///|
let _distance_model_keepalive : Array[DistanceModel] = [
Linear(ref_distance=1.0, rolloff_factor=1.0, max_distance=2.0),
LinearClamped(ref_distance=1.0, rolloff_factor=1.0, max_distance=2.0),
Inverse(ref_distance=1.0, rolloff_factor=1.0),
InverseClamped(ref_distance=1.0, rolloff_factor=1.0, max_distance=2.0),
Exponential(ref_distance=1.0, rolloff_factor=1.0),
ExponentialClamped(ref_distance=1.0, rolloff_factor=1.0, max_distance=2.0),
]
///|
pub fn DistanceModel::validate(
self : DistanceModel,
) -> Unit raise DistanceModelError {
let (ref_distance, rolloff_factor, max_distance) = match self {
None => (0.0, 0.0, 0.0)
Linear(ref_distance~, rolloff_factor~, max_distance~) => {
if ref_distance == max_distance {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, max_distance)
}
LinearClamped(ref_distance~, rolloff_factor~, max_distance~) => {
if ref_distance == max_distance {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, max_distance)
}
Inverse(ref_distance~, rolloff_factor~) => {
if ref_distance <= 0.0 {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, 0.0)
}
InverseClamped(ref_distance~, rolloff_factor~, max_distance~) => {
if ref_distance <= 0.0 {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, max_distance)
}
Exponential(ref_distance~, rolloff_factor~) => {
if ref_distance <= 0.0 {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, 0.0)
}
ExponentialClamped(ref_distance~, rolloff_factor~, max_distance~) => {
if ref_distance <= 0.0 {
raise InvalidModelParameter
}
(ref_distance, rolloff_factor, max_distance)
}
}
if ref_distance < 0.0 {
raise InvalidReferenceDistance
}
if rolloff_factor < 0.0 {
raise InvalidRolloffFactor
}
if max_distance < 0.0 {
raise InvalidMaxDistance
}
}
///|
pub struct Envelope {
attack_length : Int
attack_level : Double
fade_length : Int
fade_level : Double
}
///|
pub fn Envelope::default() -> Envelope {
{ attack_length: 0, attack_level: 1.0, fade_length: 0, fade_level: 1.0 }
}
///|
pub fn Envelope::at(self : Envelope, ticks : Int, dur : Int) -> Double {
if ticks < self.attack_length {
self.attack_level +
ticks.to_double() *
(1.0 - self.attack_level) /
self.attack_length.to_double()
} else if ticks + self.fade_length > dur {
1.0 +
(ticks + self.fade_length - dur).to_double() *
(self.fade_level - 1.0) /
self.fade_length.to_double()
} else {
1.0
}
}
///|
pub struct Replay {
after : Int
play_for : Int
with_delay : Int
}
///|
pub fn Replay::default() -> Replay {
{ after: 0, play_for: 1, with_delay: 0 }
}
///|
pub fn Replay::at(self : Replay, ticks : Int) -> Double {
if ticks >= self.play_for {
0.0
} else {
1.0
}
}
///|
pub fn Replay::dur(self : Replay) -> Int {
self.play_for + self.with_delay
}
///|
fn Replay::wrap(self : Replay, ticks : Int) -> Int? {
if ticks < self.after {
None
} else {
let d = self.dur()
if d <= 0 {
None
} else {
Some((ticks - self.after) % d)
}
}
}
///|
fn clamp_u16(x : Double) -> Int {
if x <= 0.0 || x != x {
0
} else if x >= 65535.0 {
65535
} else {
x.to_int()
}
}
///|
fn BaseEffectType::scaled(
self : BaseEffectType,
rhs : Double,
) -> BaseEffectType {
let mg = match self {
Weak(m) => m
Strong(m) => m
}
let mg2 = clamp_u16(mg.to_double() * rhs)
match self {
Weak(_) => Weak(mg2)
Strong(_) => Strong(mg2)
}
}
///|
fn BaseEffect::magnitude_at(self : BaseEffect, ticks : Int) -> BaseEffectType {
match self.scheduling.wrap(ticks) {
None => self.kind.scaled(0.0)
Some(wrapped) => {
let gate = self.scheduling.at(wrapped)
if gate == 0.0 {
self.kind.scaled(0.0)
} else {
let att = gate * self.envelope.at(wrapped, self.scheduling.play_for)
self.kind.scaled(att)
}
}
}
}
///|
fn clamp_distance(x : Double, lo : Double, hi : Double) -> Double {
let y = if x < lo { lo } else { x }
if y > hi {
hi
} else {
y
}
}
///|
fn DistanceModel::attenuation(
self : DistanceModel,
distance : Double,
) -> Double {
let mut distance = distance
match self {
Linear(ref_distance~, rolloff_factor~, max_distance~) => {
distance = if distance > max_distance { max_distance } else { distance }
1.0 -
rolloff_factor * (distance - ref_distance) / (max_distance - ref_distance)
}
LinearClamped(ref_distance~, rolloff_factor~, max_distance~) => {
distance = clamp_distance(distance, ref_distance, max_distance)
1.0 -
rolloff_factor * (distance - ref_distance) / (max_distance - ref_distance)
}
Inverse(ref_distance~, rolloff_factor~) =>
ref_distance / (ref_distance + rolloff_factor * (distance - ref_distance))
InverseClamped(ref_distance~, rolloff_factor~, max_distance~) => {
distance = clamp_distance(distance, ref_distance, max_distance)
ref_distance / (ref_distance + rolloff_factor * (distance - ref_distance))
}
Exponential(ref_distance~, rolloff_factor~) =>
@math.pow(distance / ref_distance, -rolloff_factor)
ExponentialClamped(ref_distance~, rolloff_factor~, max_distance~) => {
distance = clamp_distance(distance, ref_distance, max_distance)
@math.pow(distance / ref_distance, -rolloff_factor)
}
None => 1.0
}
}
///|
pub struct Effect {
gil : Gil
effect_token : Int
mut gamepads : Array[GamepadId]
strong : Double
weak : Double
duration_ms : Int64
base_effects : Array[BaseEffect]
mut repeat_mode : FfRepeat
mut distance_model : DistanceModel
mut position : (Double, Double, Double)
mut gain : Double
mut playing_since_ms : Int64?
}
///|
pub struct EffectBuilder {
gamepads : Array[GamepadId]
base_effects : Array[BaseEffect]
mut repeat_mode : FfRepeat
mut distance_model : DistanceModel
mut position : (Double, Double, Double)
mut gain : Double
mut strong : Double
mut weak : Double
mut duration_ms : Int64
}
///|
pub fn EffectBuilder::new() -> EffectBuilder {
{
gamepads: [],
base_effects: [],
repeat_mode: Infinitely,
distance_model: None,
position: (0.0, 0.0, 0.0),
gain: 1.0,
strong: 0.0,
weak: 0.0,
duration_ms: 1000L,
}
}
///|
fn contains_gamepad_id(ids : Array[GamepadId], id : GamepadId) -> Bool {
for x in ids {
if x.value() == id.value() {
return true
}
}
false
}
///|
pub fn EffectBuilder::gamepads(
self : EffectBuilder,
ids : Array[GamepadId],
) -> EffectBuilder {
for id in ids {
if !contains_gamepad_id(self.gamepads, id) {
self.gamepads.push(id)
}
}
self
}
///|
pub fn EffectBuilder::add_gamepad(
self : EffectBuilder,
gamepad : Gamepad,
) -> EffectBuilder {
let id = gamepad.id()
if !contains_gamepad_id(self.gamepads, id) {
self.gamepads.push(id)
}
self
}
///|
pub fn EffectBuilder::add_gamepad_id(
self : EffectBuilder,
id : GamepadId,
) -> EffectBuilder {
if !contains_gamepad_id(self.gamepads, id) {
self.gamepads.push(id)
}
self
}
///|
pub fn EffectBuilder::rumble(
self : EffectBuilder,
strong : Double,
weak : Double,
) -> EffectBuilder {
self.strong = strong
self.weak = weak
self
}
///|
pub fn EffectBuilder::duration(
self : EffectBuilder,
duration_ms : Int64,
) -> EffectBuilder {
self.duration_ms = duration_ms
self
}
///|
pub fn EffectBuilder::add_effect(
self : EffectBuilder,
effect : BaseEffect,
) -> EffectBuilder {
self.base_effects.push(effect)
self
}
///|
pub fn EffectBuilder::repeat(
self : EffectBuilder,
repeat_mode : FfRepeat,
) -> EffectBuilder {
self.repeat_mode = repeat_mode
self
}
///|
pub fn EffectBuilder::distance_model(
self : EffectBuilder,
model : DistanceModel,
) -> EffectBuilder {
self.distance_model = model
self
}
///|
pub fn EffectBuilder::position(
self : EffectBuilder,
position : (Double, Double, Double),
) -> EffectBuilder {
self.position = position
self
}
///|
pub fn EffectBuilder::gain(
self : EffectBuilder,
gain : Double,
) -> EffectBuilder {
self.gain = if gain < 0.0 { 0.0 } else { gain }
self
}
///|
///|
pub fn EffectBuilder::finish(
self : EffectBuilder,
gil : Gil,
) -> Effect raise FfError {
if self.gamepads.length() == 0 {
raise NoGamepads
}
// Validate: connected + ff supported.
for id in self.gamepads {
match gil.gamepad(id) {
None => raise Disconnected(id)
Some(gp) =>
if !gp.is_connected() {
raise Disconnected(id)
} else if !gp.is_ff_supported() {
raise FfNotSupported(id)
}
}
}
// Must have a native backend to talk to.
match gil.backend {
None => raise NotSupported
Some(_) => ()
}
self.distance_model.validate() catch {
err => raise InvalidDistanceModel(err)
}
let effect : Effect = {
gil,
effect_token: gil.new_effect_token(),
gamepads: self.gamepads,
strong: self.strong,
weak: self.weak,
duration_ms: self.duration_ms,
base_effects: self.base_effects,
repeat_mode: self.repeat_mode,
distance_model: self.distance_model,
position: self.position,
gain: self.gain,
playing_since_ms: None,
}
gil.ff_upsert_effect_from_handle(effect)
effect
}
///|
pub fn Effect::set_gamepads(
self : Effect,
ids : Array[GamepadId],
gil : Gil,
) -> Unit raise FfError {
let out : Array[GamepadId] = []
for id in ids {
match gil.connected_gamepad(id) {
None => raise Disconnected(id)
Some(gp) =>
if !gp.is_ff_supported() {
raise FfNotSupported(id)
} else if !contains_gamepad_id(out, id) {
out.push(id)
}
}
}
self.gamepads = out
self.gil.ff_set_effect_gamepads(self.effect_token, out)
}
///|
pub fn Effect::add_gamepad(
self : Effect,
gamepad : Gamepad,
) -> Unit raise FfError {
let id = gamepad.id()
if !gamepad.is_connected() {
raise Disconnected(id)
}
if !gamepad.is_ff_supported() {
raise FfNotSupported(id)
}
if !contains_gamepad_id(self.gamepads, id) {
self.gamepads.push(id)
}
self.gil.ff_set_effect_gamepads(self.effect_token, self.gamepads)
}
///|
pub fn Effect::set_gain(self : Effect, gain : Double) -> Unit {
self.gain = if gain < 0.0 { 0.0 } else { gain }
self.gil.ff_set_effect_gain(self.effect_token, self.gain)
}
///|
pub fn Effect::set_repeat(self : Effect, repeat_mode : FfRepeat) -> Unit {
self.repeat_mode = repeat_mode
self.gil.ff_set_effect_repeat(self.effect_token, repeat_mode)
}
///|
pub fn Effect::set_distance_model(
self : Effect,
model : DistanceModel,
) -> Unit raise FfError {
model.validate() catch {
err => raise InvalidDistanceModel(err)
}
self.distance_model = model
self.gil.ff_set_effect_distance_model(self.effect_token, model)
}
///|
pub fn Effect::set_position(
self : Effect,
position : (Double, Double, Double),
) -> Unit {
self.position = position
self.gil.ff_set_effect_position(self.effect_token, position)
}
///|
pub fn Effect::play(self : Effect) -> Unit raise FfError {
if self.gil.backend is None {
raise NotSupported
}
self.gil.ff_upsert_effect_from_handle(self)
let now = runtime_now_ms()
let tick = self.gil.ff_now_tick(now)
self.playing_since_ms = Some(now)
self.gil.ff_play_effect(self.effect_token, tick)
self.gil.ff_tick_update(now, true)
}
///|
pub fn Effect::stop(self : Effect) -> Unit raise FfError {
if self.gil.backend is None {
raise NotSupported
}
self.gil.ff_upsert_effect_from_handle(self)
self.playing_since_ms = None
self.gil.ff_stop_effect(self.effect_token)
let now = runtime_now_ms()
self.gil.ff_tick_update(now, true)
}