Files
ReSkate/Extension/Profile/local_profile_objects.cpp
AldoandClaude Opus 5.5 57d98307a7 Skater: FPV drone mode
An FPV drone flown from the free camera: real quad physics (thrust,
gravity, drag, Betaflight "Actual" rates), collisions with the world, and
a goggle-style display. Open it with `dronemenu` (or the drone window's
Start flying).

Flying
- Keyboard and mouse, gamepads, or USB RC radios (EdgeTX etc. in Joystick
  mode). "Set up controller" learns each stick and switch by asking for
  one at a time, reversed axes and springing gamepad throttles included.
  While a controller flies the drone, the game is kept off it, as
  freecam's controller mode does.
- Presets (Tinywhoop, Toothpick, 5 inch), arm/disarm, back to takeoff,
  and every setting live through `dronetune`, saved in the profile.
- Optional range: the picture breaks up and is lost with distance from
  the takeoff point; past it, failsafe cuts the motors.
- OSD, on-screen sticks, an analogue video look, synthesised motor sound.
- Goggles: the pilot's skater wears eyewear from the game's catalog while
  flying, and gets their own back on landing (Road Rash aware).
- Start flying hides the game UI, Stop flying brings it back.

Multiplayer
- New packet `drone` (36): where a player's drone is and which way it
  faces, 15 times a second, unreliable, relayed like effects. Markers
  glide between updates. Protocol version 52.
- FLYING DRONE above the name of a player who is flying.
- Players choose whether to share their drone and whether to see others'.
- Dedicated servers: `sync_drones` (players section) and `drones on|off`.
  Off: no drone packets are relayed, and players are told in the roster.

Changes to existing code
- `flyspeed` takes any speed from 0.6 to 1500, not only the seven listed
  (the drone's presets and top speed slider use speeds of their own).
- Overlay: begin/end_input_access, so the drone's controller read sees
  the device while the game is kept off it.
- Profile: local_surface_hit, the park editor's ray from one point to
  another, for the drone's collisions.

Tests: Skater/Test/drone_radio_tests (controller setup), and the drone
packet and roster rule in Multiplayer/Test/protocol_tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MArBL6UbYZPsp9rMnq2Ks5
2026-10-13 21:59:49 +01:00

114 lines
5.9 KiB
C++

#include "runtime_internal.h"
#include "Extension/Objects/local_placements_runtime.h"
#include "Extension/Objects/ParkEditor/park_editor_runtime.h"
#include "Extension/Objects/ParkEditor/park_editor_surface.h"
#include <cmath>
#include "Extension/World/local_world_layers.h"
namespace dingosdk {
using namespace profile_runtime;
ObjectPersistenceModel local_profile_object_persistence() {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
ObjectPersistenceModel model;
model.available = local_runtime().active.load() && r.store && !r.failed && !r.save_failed;
model.enabled = r.enabled; model.map = r.map; model.clearing = r.clearing;
if (const auto it = r.document.maps.find(r.map); it != r.document.maps.end()) model.count = it->second.size();
model.busy = r.clearing || r.inflight.has_value();
for (const auto& row : r.rows) model.rows.push_back({row.token, row.object.item, row.object.position, row.saved_id != 0, row.spawned});
model.can_clear = model.available && world_layers_runtime().model.ready && !r.map.empty() &&
!model.busy && model.count != 0;
model.status = r.status;
if (r.save_failed) model.status = "Object save failed. The previous file is retained; restart after checking objects.sqlite3.";
if (!r.store || r.failed) model.status = "Object persistence is unavailable. Check objects.sqlite3 and the runtime log.";
return model;
}
bool set_local_object_persistence(bool enabled) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !r.store || r.failed || r.save_failed || park_editor_owns_placements()) return false;
try {
local_runtime().store->set_bool_option("ReSkate.ObjectPersistence", enabled);
r.enabled = enabled;
r.status = enabled ? "Object persistence enabled." : "Object persistence disabled. Saved layouts are retained.";
return true;
} catch (...) { r.status = "Could not save the object persistence option."; return false; }
}
bool clear_local_persisted_objects(std::string_view map) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !placement_session_ready() || r.map != map) return false;
std::set<std::uint64_t> tokens;
for (const auto& row : r.rows) if (row.saved_id) tokens.insert(row.token);
return begin_placement_delete(tokens);
}
bool delete_local_placed_object(std::string_view map, std::uint64_t token) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !placement_session_ready() || r.map != map ||
std::none_of(r.rows.begin(), r.rows.end(), [&](const auto& row) { return row.token == token; })) return false;
return begin_placement_delete({token});
}
bool teleport_local_skater(const std::array<float, 3>& position) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !r.teleport || !r.transition_ctor || !r.transition_destroy) return false;
for (const auto v : position)
if (!std::isfinite(v) || std::abs(v) > 1e6f) return false;
r.position_teleport = position;
r.position_teleport_at = GetTickCount64();
return true;
}
std::optional<float> local_ground_height(float x, float z, float top, float bottom) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !r.context) return {};
for (const auto v : {x, z, top, bottom})
if (!std::isfinite(v) || std::abs(v) > 1e5f) return {};
if (!(top > bottom)) return {};
static std::uintptr_t api_base{};
static editor::NativeSurfaceApi api;
if (api_base != local_runtime().base) { api = editor::surface_api(local_runtime().base); api_base = local_runtime().base; }
if (!api.ready) return {};
// The client physics world, resolved from the client TLS context like the park editor's
// surface probe (the server context has no world).
std::array<std::uintptr_t, 2> context{};
r.context(context.data());
if (!context[0]) return {};
const auto world = api.world(context[0]);
if (!world) return {};
// Closest hit from the top: the topmost surface (a roof or deck above the street wins).
const auto hit = editor::cast_surface(api, world, {x, top, z}, {x, bottom, z});
if (!hit) return {};
return (*hit)[1];
}
std::optional<std::array<float, 3>> local_surface_hit(const std::array<float, 3>& from, const std::array<float, 3>& to) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !r.context) return {};
for (const auto v : {from[0], from[1], from[2], to[0], to[1], to[2]})
if (!std::isfinite(v) || std::abs(v) > 1e5f) return {};
static std::uintptr_t api_base{};
static editor::NativeSurfaceApi api;
if (api_base != local_runtime().base) { api = editor::surface_api(local_runtime().base); api_base = local_runtime().base; }
if (!api.ready) return {};
std::array<std::uintptr_t, 2> context{};
r.context(context.data());
if (!context[0]) return {};
const auto world = api.world(context[0]);
if (!world) return {};
const auto hit = editor::cast_surface(api, world, {from[0], from[1], from[2]}, {to[0], to[1], to[2]});
if (!hit) return {};
return std::array<float, 3>{(*hit)[0], (*hit)[1], (*hit)[2]};
}
bool teleport_to_local_placed_object(std::string_view map, std::uint64_t token) {
std::lock_guard lock(local_runtime().native_mutex);
auto& r = placements_runtime();
if (!local_runtime().active || !placement_session_ready() || r.map != map || r.clearing ||
std::none_of(r.rows.begin(), r.rows.end(), [&](const auto& row) { return row.token == token; })) return false;
r.teleport_token = token; r.teleport_queued_at = GetTickCount64();
r.status = "Teleport queued...";
return true;
}
}