Skip to content

Commit

Permalink
👼Script: FreeForces 💪
Browse files Browse the repository at this point in the history
FreeForces are global, persistent forces which act upon one node of one actor each. They exist separately from actors and their existing hook/tie/rope interactions. This means they do not interfere with actor N/B or any of the existing linking logic. They're intended for creating special physics effects which aren't supported otherwise, like making party baloons float. They're intentionally only accessible from scripts to maximize their flexibility and limit maintenance of the codebase.

Keep in mind that the physics simulation runs at 2khz (2000 steps per second), while scripts run in sync with FPS. That's why there's no option to add temporary/one-off force impulses from scripts - you are responsible for adding the persistent force, monitoring and adjusting it as needed, and then removing it when no longer necessary. You can create the forces as dummy and then change their type later as needed.

To manage FreeForces, you use `game.pushMessage()` with `MSG_SIM_ADD_FREEFORCE_REQUESTED`, `MSG_SIM_MODIFY_FREEFORCE_REQUESTED` or `MSG_SIM_REMOVE_FREEFORCE_REQUESTED`.
Parameters common to ADD/MODIFY requests:
* 'id' (int) - unique ID of this freeforce, use `game.getFreeForceNextId()` to obtain an unique sequential ID.
* 'type' (FreeForceType) - One of `FREEFORCETYPE_DUMMY`, `FREEFORCETYPE_CONSTANT`, `FREEFORCETYPE_TOWARDS_COORDS`, `FREEFORCETYPE_TOWARDS_NODE`
* 'base_actor' (int) - Unique actor instance ID, obtain it by `game.getCurrentTruck().getInstanceId()`, or if you're in an actor-script, then you can do `thisActor.getInstanceId()` // `BeamClass@ thisActor`is automatic global variable for scripts bound to actors.
* 'base_node' (int) - Node number (0 - 65535)
* 'force_magnitude' (float) - the amount of force to apply, in Newton/meters
Parameters for `FREEFORCETYPE_CONSTANT`:
* 'force_const_direction' (vector3) - constant force direction. Note that  Y=up, so to make things float, use vector3(0, 1, 0).
Parameters for `FREEFORCETYPE_TOWARDS_COORDS`:
* 'target_coords' (vector3) - world position to target. For example `game.getPersonPosition()` will target it at position where character is standing AT THE MOMENT.
Parameters for `FREEFORCETYPE_TOWARDS_NODE`:
* 'target_actor' (int) - Unique actor instance ID.
* 'target_node' (int) - Node number (0 - 65535).

Note when any of the actors involved in a FreeForce (be it the base one or target one), the freeforce is deleted.

Example from the PartyBaloon demo mod:
```
    // Create the up-force
    upforce_assigned_id = game.getFreeForceNextId();
    game.pushMessage(MSG_SIM_ADD_FREEFORCE_REQUESTED, {
        {'id', upforce_assigned_id },
        {'type', FREEFORCETYPE_CONSTANT },
        {'base_actor', thisActor.getInstanceId() },  // `BeamClass@ thisActor`is automatic global variable for scripts bound to actors.
        {'base_node', UPFORCE_NODE },
        {'force_const_direction', vector3(0, 1, 0) }, // Y=up
        {'force_magnitude', 0.4f } // Newton/meters
    });
```
  • Loading branch information
ohlidalp committed Jun 17, 2024
1 parent cfeba57 commit 79d9068
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 19 deletions.
9 changes: 6 additions & 3 deletions source/main/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -591,9 +591,12 @@ const char* MsgTypeToString(MsgType type)
case MSG_SIM_TELEPORT_PLAYER_REQUESTED : return "MSG_SIM_TELEPORT_PLAYER_REQUESTED";
case MSG_SIM_HIDE_NET_ACTOR_REQUESTED : return "MSG_SIM_HIDE_NET_ACTOR_REQUESTED";
case MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED : return "MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED";
case MSG_SIM_SCRIPT_EVENT_TRIGGERED : return "MSG_SIM_SCRIPT_EVENT_TRIGGERED";
case MSG_SIM_SCRIPT_CALLBACK_QUEUED : return "MSG_SIM_SCRIPT_CALLBACK_QUEUED";
case MSG_SIM_ACTOR_LINKING_REQUESTED : return "MSG_SIM_ACTOR_LINKING_REQUESTED";
case MSG_SIM_SCRIPT_EVENT_TRIGGERED : return "MSG_SIM_SCRIPT_EVENT_TRIGGERED";
case MSG_SIM_SCRIPT_CALLBACK_QUEUED : return "MSG_SIM_SCRIPT_CALLBACK_QUEUED";
case MSG_SIM_ACTOR_LINKING_REQUESTED : return "MSG_SIM_ACTOR_LINKING_REQUESTED";
case MSG_SIM_ADD_FREEFORCE_REQUESTED : return "MSG_SIM_ADD_FREEFORCE_REQUESTED";
case MSG_SIM_MODIFY_FREEFORCE_REQUESTED : return "MSG_SIM_MODIFY_FREEFORCE_REQUESTED";
case MSG_SIM_REMOVE_FREEFORCE_REQUESTED : return "MSG_SIM_REMOVE_FREEFORCE_REQUESTED";

case MSG_GUI_OPEN_MENU_REQUESTED : return "MSG_GUI_OPEN_MENU_REQUESTED";
case MSG_GUI_CLOSE_MENU_REQUESTED : return "MSG_GUI_CLOSE_MENU_REQUESTED";
Expand Down
3 changes: 3 additions & 0 deletions source/main/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ enum MsgType
MSG_SIM_SCRIPT_EVENT_TRIGGERED, //!< Payload = RoR::ScriptEventArgs* (owner)
MSG_SIM_SCRIPT_CALLBACK_QUEUED, //!< Payload = RoR::ScriptCallbackArgs* (owner)
MSG_SIM_ACTOR_LINKING_REQUESTED, //!< Payload = RoR::ActorLinkingRequest* (owner)
MSG_SIM_ADD_FREEFORCE_REQUESTED, //!< Payload = RoR::FreeForceRequest* (owner)
MSG_SIM_MODIFY_FREEFORCE_REQUESTED, //!< Payload = RoR::FreeForceRequest* (owner)
MSG_SIM_REMOVE_FREEFORCE_REQUESTED, //!< Payload = RoR::FreeForceID_t* (owner)
// GUI
MSG_GUI_OPEN_MENU_REQUESTED,
MSG_GUI_CLOSE_MENU_REQUESTED,
Expand Down
6 changes: 5 additions & 1 deletion source/main/ForwardDeclarations.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
namespace RoR
{
typedef int ActorInstanceID_t; //!< Unique sequentially generated ID of an actor in session. Use `ActorManager::GetActorById()`
static const ActorInstanceID_t ACTORINSTANCEID_INVALID = -1;
static const ActorInstanceID_t ACTORINSTANCEID_INVALID = 0;

typedef int ScriptUnitId_t; //!< Unique sequentially generated ID of a loaded and running scriptin session. Use `ScriptEngine::getScriptUnit()`
static const ScriptUnitId_t SCRIPTUNITID_INVALID = -1;
Expand All @@ -51,6 +51,7 @@ namespace RoR

typedef uint16_t NodeNum_t; //!< Node position within `Actor::ar_nodes`; use RoR::NODENUM_INVALID as empty value.
static const NodeNum_t NODENUM_INVALID = std::numeric_limits<NodeNum_t>::max();
static const NodeNum_t NODENUM_MAX = std::numeric_limits<NodeNum_t>::max() - 1;

typedef int WheelID_t; //!< Index to `Actor::ar_wheels`, `use RoR::WHEELID_INVALID` as empty value
static const WheelID_t WHEELID_INVALID = -1;
Expand All @@ -61,6 +62,9 @@ namespace RoR
typedef int FlexbodyID_t; //!< Index to `GfxActor::m_flexbodies`, `use RoR::FLEXBODYID_INVALID` as empty value
static const FlexbodyID_t FLEXBODYID_INVALID = -1;

typedef int FreeForceID_t; //!< Unique sequentially generated ID of `FreeForce`; use `ActorManager::GetFreeForceNextId()`.
static const FreeForceID_t FREEFORCEID_INVALID = -1;

typedef int FlareID_t; //!< Index into `Actor::ar_flares`, use `RoR::FLAREID_INVALID` as empty value
static const FlareID_t FLAREID_INVALID = -1;

Expand Down
12 changes: 0 additions & 12 deletions source/main/GameContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,18 +462,6 @@ void GameContext::DeleteActor(ActorPtr actor)

TRIGGER_EVENT_ASYNC(SE_GENERIC_DELETED_TRUCK, actor->ar_instance_id);

// Unload actor's scripts
std::vector<ScriptUnitId_t> unload_list;
for (auto& pair : App::GetScriptEngine()->getScriptUnits())
{
if (pair.second.associatedActor == actor)
unload_list.push_back(pair.first);
}
for (ScriptUnitId_t id : unload_list)
{
App::GetScriptEngine()->unloadScript(id);
}

m_actor_manager.DeleteActorInternal(actor);
}

Expand Down
8 changes: 8 additions & 0 deletions source/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,14 @@ int main(int argc, char *argv[])
break;
}

case MSG_SIM_ADD_FREEFORCE_REQUESTED:
{
FreeForceRequest* rq = static_cast<FreeForceRequest*>(m.payload);
App::GetGameContext()->GetActorManager()->AddFreeForce(rq);
delete rq;
break;
}

// -- GUI events ---

case MSG_GUI_OPEN_MENU_REQUESTED:
Expand Down
181 changes: 180 additions & 1 deletion source/main/physics/ActorManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
using namespace Ogre;
using namespace RoR;

static ActorInstanceID_t m_actor_counter = 0;
static ActorInstanceID_t m_actor_counter = 1;
const ActorPtr ActorManager::ACTORPTR_NULL; // Dummy value to be returned as const reference.

ActorManager::ActorManager()
Expand Down Expand Up @@ -946,6 +946,26 @@ void ActorManager::DeleteActorInternal(ActorPtr actor)
}
#endif // USE_SOCKETW

// Unload actor's scripts
std::vector<ScriptUnitId_t> unload_list;
for (auto& pair : App::GetScriptEngine()->getScriptUnits())
{
if (pair.second.associatedActor == actor)
unload_list.push_back(pair.first);
}
for (ScriptUnitId_t id : unload_list)
{
App::GetScriptEngine()->unloadScript(id);
}

// Remove FreeForces referencing this actor
m_free_forces.erase(
std::remove_if(
m_free_forces.begin(),
m_free_forces.end(),
[actor](FreeForce& item) { return item.ffc_base_actor == actor || item.ffc_target_actor == actor; }),
m_free_forces.end());

// Only dispose(), do not `delete`; a script may still hold pointer to the object.
actor->dispose();

Expand Down Expand Up @@ -1234,6 +1254,9 @@ void ActorManager::UpdatePhysicsSimulation()
}
App::GetThreadPool()->Parallelize(tasks);
}

// Apply FreeForces - intentionally as a separate pass over all actors
this->CalcFreeForces();
}
for (ActorPtr& actor: m_actors)
{
Expand Down Expand Up @@ -1520,3 +1543,159 @@ void ActorManager::UpdateTruckFeatures(ActorPtr vehicle, float dt)
BITMASK_SET(vehicle->m_lightmask, RoRnet::LIGHTMASK_REVERSE, (vehicle->ar_engine && vehicle->ar_engine->GetGear() < 0));
}

void ActorManager::CalcFreeForces()
{
for (FreeForce& freeforce: m_free_forces)
{
// Sanity checks
ROR_ASSERT(freeforce.ffc_base_actor != nullptr);
ROR_ASSERT(freeforce.ffc_base_actor->ar_state != ActorState::DISPOSED);
ROR_ASSERT(freeforce.ffc_base_node != NODENUM_INVALID);
ROR_ASSERT(freeforce.ffc_base_node <= freeforce.ffc_base_actor->ar_num_nodes);


switch (freeforce.ffc_type)
{
case FreeForceType::CONSTANT:
freeforce.ffc_base_actor->ar_nodes[freeforce.ffc_base_node].Forces += freeforce.ffc_force_magnitude * freeforce.ffc_force_const_direction;
break;

case FreeForceType::TOWARDS_COORDS:
{
const Vector3 force_direction = (freeforce.ffc_target_coords - freeforce.ffc_base_actor->ar_nodes[freeforce.ffc_base_node].AbsPosition).normalisedCopy();
freeforce.ffc_base_actor->ar_nodes[freeforce.ffc_base_node].Forces += freeforce.ffc_force_magnitude * force_direction;
}
break;

case FreeForceType::TOWARDS_NODE:
{
// Sanity checks
ROR_ASSERT(freeforce.ffc_target_actor != nullptr);
ROR_ASSERT(freeforce.ffc_target_actor->ar_state != ActorState::DISPOSED);
ROR_ASSERT(freeforce.ffc_target_node != NODENUM_INVALID);
ROR_ASSERT(freeforce.ffc_target_node <= freeforce.ffc_target_actor->ar_num_nodes);

const Vector3 force_direction = (freeforce.ffc_target_actor->ar_nodes[freeforce.ffc_target_node].AbsPosition - freeforce.ffc_base_actor->ar_nodes[freeforce.ffc_base_node].AbsPosition).normalisedCopy();
freeforce.ffc_base_actor->ar_nodes[freeforce.ffc_base_node].Forces += freeforce.ffc_force_magnitude * force_direction;
}
break;

default:
break;
}
}
}

static bool ProcessFreeForce(FreeForceRequest* rq, FreeForce& freeforce)
{
// internal helper for processing add/modify requests, with checks
// ---------------------------------------------------------------

// Unchecked stuff
freeforce.ffc_id = (FreeForceID_t)rq->ffr_id;
freeforce.ffc_type = (FreeForceType)rq->ffr_type;
freeforce.ffc_force_magnitude = (float)rq->ffr_force_magnitude;
freeforce.ffc_force_const_direction = rq->ffr_force_const_direction;
freeforce.ffc_target_coords = rq->ffr_target_coords;

// Base actor
freeforce.ffc_base_actor = App::GetGameContext()->GetActorManager()->GetActorById(rq->ffr_base_actor);
ROR_ASSERT(freeforce.ffc_base_actor != nullptr && freeforce.ffc_base_actor->ar_state != ActorState::DISPOSED);
if (!freeforce.ffc_base_actor || freeforce.ffc_base_actor->ar_state == ActorState::DISPOSED)
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot add free force with ID {} to actor {}: Base actor not found or disposed", freeforce.ffc_id, rq->ffr_base_actor));
return false;
}

// Base node
ROR_ASSERT(rq->ffr_base_node >= 0);
ROR_ASSERT(rq->ffr_base_node <= NODENUM_MAX);
ROR_ASSERT(rq->ffr_base_node <= freeforce.ffc_base_actor->ar_num_nodes);
if (rq->ffr_base_node < 0 || rq->ffr_base_node >= NODENUM_MAX || rq->ffr_base_node >= freeforce.ffc_base_actor->ar_num_nodes)
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot add free force with ID {} to actor {}: Invalid base node number {}", freeforce.ffc_id, rq->ffr_base_actor, rq->ffr_base_node));
return false;
}
freeforce.ffc_base_node = (NodeNum_t)rq->ffr_base_node;

if (freeforce.ffc_type == FreeForceType::TOWARDS_NODE)
{
// Target actor
freeforce.ffc_target_actor = App::GetGameContext()->GetActorManager()->GetActorById(rq->ffr_target_actor);
ROR_ASSERT(freeforce.ffc_target_actor != nullptr && freeforce.ffc_target_actor->ar_state != ActorState::DISPOSED);
if (!freeforce.ffc_target_actor || freeforce.ffc_target_actor->ar_state == ActorState::DISPOSED)
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot add free force of type 'TOWARDS_NODE' with ID {} to actor {}: Target actor not found or disposed", freeforce.ffc_id, rq->ffr_target_actor));
return false;
}

// Target node
ROR_ASSERT(rq->ffr_target_node >= 0);
ROR_ASSERT(rq->ffr_target_node <= NODENUM_MAX);
ROR_ASSERT(rq->ffr_target_node <= freeforce.ffc_target_actor->ar_num_nodes);
if (rq->ffr_target_node < 0 || rq->ffr_target_node >= NODENUM_MAX || rq->ffr_target_node >= freeforce.ffc_target_actor->ar_num_nodes)
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot add free force of type 'TOWARDS_NODE' with ID {} to actor {}: Invalid target node number {}", freeforce.ffc_id, rq->ffr_target_actor, rq->ffr_target_node));
return false;
}
freeforce.ffc_target_node = (NodeNum_t)rq->ffr_target_node;
}

return true;
}

ActorManager::FreeForceVec_t::iterator ActorManager::FindFreeForce(FreeForceID_t id)
{
return std::find_if(m_free_forces.begin(), m_free_forces.end(), [id](FreeForce& item) { return id == item.ffc_id; });
}

void ActorManager::AddFreeForce(FreeForceRequest* rq)
{
// Make sure ID is unique
if (this->FindFreeForce(rq->ffr_id) != m_free_forces.end())
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot add free force with ID {}: ID already in use", rq->ffr_id));
return;
}

FreeForce freeforce;
if (ProcessFreeForce(rq, freeforce))
{
m_free_forces.push_back(freeforce);
}
}

void ActorManager::ModifyFreeForce(FreeForceRequest* rq)
{
auto it = this->FindFreeForce(rq->ffr_id);
if (it == m_free_forces.end())
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot modify free force with ID {}: ID not found", rq->ffr_id));
return;
}

FreeForce& freeforce = *it;
if (ProcessFreeForce(rq, freeforce))
{
*it = freeforce;
}
}

void ActorManager::RemoveFreeForce(FreeForceID_t id)
{
auto it = this->FindFreeForce(id);
if (it == m_free_forces.end())
{
App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR,
fmt::format("Cannot remove free force with ID {}: ID not found", id));
return;
}

m_free_forces.erase(it);
}
19 changes: 17 additions & 2 deletions source/main/physics/ActorManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ class ActorManager
{
public:

typedef std::vector<FreeForce> FreeForceVec_t;

ActorManager();
~ActorManager();

/// @name Lifetime
/// @name Actor lifetime
/// @{
ActorPtr CreateNewActor(ActorSpawnRequest rq, RigDef::DocumentPtr def);
void DeleteActorInternal(ActorPtr actor); //!< Do not call directly; use `GameContext::DeleteActor()`
/// @}

/// @name Lookup
/// @name Actor lookup
/// @{
const ActorPtr& GetActorById(ActorInstanceID_t actor_id);
const ActorPtr& GetActorByNetworkLinks(int source_id, int stream_id); // used by character
Expand All @@ -63,6 +65,15 @@ class ActorManager
const ActorPtr& FetchRescueVehicle();
/// @}

/// @name Free forces
/// @{
void AddFreeForce(FreeForceRequest* rq);
void ModifyFreeForce(FreeForceRequest* rq);
void RemoveFreeForce(FreeForceID_t id);
FreeForceVec_t::iterator FindFreeForce(FreeForceID_t id);
FreeForceID_t GetFreeForceNextId() { return m_free_force_next_id++; }
/// @}

void UpdateActors(ActorPtr player_actor);
void SyncWithSimThread();
void UpdatePhysicsSimulation();
Expand All @@ -84,6 +95,7 @@ class ActorManager
void SetSimulationPaused(bool v) { m_simulation_paused = v; }
float GetTotalTime() const { return m_total_sim_time; }
RoR::CmdKeyInertiaConfig& GetInertiaConfig() { return m_inertia_config; }


void CleanUpSimulation(); //!< Call this after simulation loop finishes.

Expand Down Expand Up @@ -126,6 +138,7 @@ class ActorManager
void RecursiveActivation(int j, std::vector<bool>& visited);
void ForwardCommands(ActorPtr source_actor); //!< Fowards things to trailers
void UpdateTruckFeatures(ActorPtr vehicle, float dt);
void CalcFreeForces(); //!< Apply FreeForces - intentionally as a separate pass over all actors

// Networking
std::map<int, std::set<int>> m_stream_mismatches; //!< Networking: A set of streams without a corresponding actor in the actor-array for each stream source
Expand All @@ -142,6 +155,8 @@ class ActorManager
float m_simulation_time = 0.f; //!< Amount of time the physics simulation is going to be advanced
bool m_simulation_paused = false;
float m_total_sim_time = 0.f;
FreeForceVec_t m_free_forces; //!< Global forces added ad-hoc by scripts
FreeForceID_t m_free_force_next_id = 0; //!< Unique ID for each FreeForce

// Utils
std::unique_ptr<ThreadPool> m_sim_thread_pool;
Expand Down
Loading

0 comments on commit 79d9068

Please sign in to comment.