Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/configuration/example_community_patch_settings_en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,20 @@ zoom_label_player_threshold = 0.5
# This controls the zoom speed when using Q/E in System View, defaults to 350
keyboard_zoom_speed = 350.0

# Controls mouse/touch and keyboard panning speed in System view only.
# 1.0 = normal, 0.5 = half speed, 1.5 = 50% faster. Valid range: 0.1 to 10.
# Invalid values, including zero and values below 0.1, use normal speed (1.0).
# Replaces system_pan_momentum, which was unused. Does not require experimental mode.
system_pan_speed = 1.0

# Independent mouse/touch and keyboard panning speed in Galaxy view, with the same range.
galaxy_pan_speed = 1.0

# Controls the system pan momentum speed. Defaults to 0.4. Read with enable_experimental, but not applied yet
system_pan_momentum = 0.4

# Controls how quickly the momentum falls off when letting go of the mouse. Lower = quicker stop. Defaults to 0.8
# Momentum retained each update after releasing the mouse, touch or movement keys.
# Lower = quicker stop; 0 = immediate stop; 0.99 = long glide. Clamped to 0 to 0.99. Defaults to 0.8.
system_pan_momentum_falloff = 0.8

# Subgroup: Loader
Expand Down
14 changes: 13 additions & 1 deletion src/mod/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,21 @@ void Config::Read(const toml::table& file, toml::table& effective, bool log_valu
r.Read("graphics", "free_resize", g.free_resize);
r.Read("graphics", "allow_cursor", g.allow_cursor);
r.Read("graphics", "keyboard_zoom_speed", g.keyboard_zoom_speed);
r.ReadValid("graphics", "system_pan_speed", g.system_pan_speed,
[](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; });
r.ReadValid("graphics", "galaxy_pan_speed", g.galaxy_pan_speed,
[](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; });
if (control.enable_experimental)
r.Read("graphics", "system_pan_momentum", g.system_pan_momentum);
r.Read("graphics", "system_pan_momentum_falloff", g.system_pan_momentum_falloff);
r.ReadValid("graphics", "system_pan_momentum_falloff", g.system_pan_momentum_falloff,
[](float v) { return std::isfinite(v); });
const auto bounded_falloff = std::clamp(g.system_pan_momentum_falloff, 0.0f, 0.99f);
if (g.system_pan_momentum_falloff != bounded_falloff) {
spdlog::warn("config graphics.system_pan_momentum_falloff: '{}' clamped to {}", g.system_pan_momentum_falloff,
bounded_falloff);
g.system_pan_momentum_falloff = bounded_falloff;
r.Record("graphics", "system_pan_momentum_falloff", bounded_falloff);
}
Comment on lines 617 to +627

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One part is marked Experimental, which means it won't take effect where the rest does? This seems inconsistent to me. Either we should make it all experimental or none.

r.Read("graphics", "disable_target_camera_lock", g.disable_target_camera_lock);
r.Read("graphics", "borderless_fullscreen", g.borderless_fullscreen);
r.Read("graphics", "transition_time", g.transition_time);
Expand Down
4 changes: 3 additions & 1 deletion src/mod/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ class Config final
bool free_resize = true;
bool allow_cursor = true;
float keyboard_zoom_speed = 350.0f;
// TODO(tashcan): read (with enable_experimental) but nothing applies it.
float system_pan_speed = 1.0f;
float galaxy_pan_speed = 1.0f;
// Retained compatibility key; use system_pan_speed for panning speed.
float system_pan_momentum = 0.4f;
// How much of the pan speed is kept each frame after a drag or key is released.
float system_pan_momentum_falloff = 0.8f;
Expand Down
76 changes: 65 additions & 11 deletions src/mod/features/camera.cc
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,49 @@ void PlanetViewUtils_Populate_Hook(auto original, PlanetViewUtils* _this, Il2Cpp
bool PointerHeld()
{ return unity::Input::GetMouseButton(0) || unity::Input::touchCount() > 0; }

// Native LateUpdate uses deltaScaler for keyboard movement and coasting. Supply
// the configured fade once while preserving native keyboard amplitude.
struct NativePanScalers {
NavigationPan* pan;
float delta, keyboard;

NativePanScalers(NavigationPan* value, float speed)
: pan(value)
, delta(value->deltaScaler())
, keyboard(value->keyboardDeltaScaler())
{
pan->SetDeltaScaler(1.0f);
pan->SetKeyboardDeltaScaler(keyboard * delta * speed);
}

~NativePanScalers()
{
pan->SetDeltaScaler(delta);
pan->SetKeyboardDeltaScaler(keyboard);
}
};

float CameraPanSpeed(NodeDepth depth, float system_speed, float galaxy_speed)
{
switch (depth) {
case NodeDepth::SolarSystem:
return system_speed;
case NodeDepth::Galaxy:
return galaxy_speed;
default:
return 1.0f;
}
}

void NavigationPan_MoveCamera_Hook(auto original, NavigationPan* _this, unity::Vector2 delta, bool momentum)
{
if (!momentum && _this->panningStarted()) {
const auto& graphics = Config::Get().graphics;
delta = delta * CameraPanSpeed(_this->nodeDepth(), graphics.system_pan_speed, graphics.galaxy_pan_speed);
}
original(_this, delta, momentum);
}

// Replaces the game's LateUpdate: the keyboard source is the mod's shortcuts, and the pan keeps gliding after the
// drag or key is released. Each frame moves the camera once: by the keys while one is held, otherwise by the
// glide.
Expand All @@ -271,20 +314,27 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this)
// until the arrow is released. A modified move binding such as CTRL-LEFT still pans.
const bool keyboard = config.control.hotkeys_enabled && !config.ui.disable_move_keys;

// With the game's own hotkeys, its LateUpdate pans and glides by itself. The mod still applies its momentum falloff
// to that glide, without moving the camera a second time.
const bool native = keyboard && (config.control.use_scopely_hotkeys || !config.patches.hotkeyhooks);
if (native)
original(_this);
const bool native = keyboard && (config.control.use_scopely_hotkeys || !config.patches.hotkeyhooks);
const float pan_speed =
CameraPanSpeed(_this->nodeDepth(), config.graphics.system_pan_speed, config.graphics.galaxy_pan_speed);

auto d = _this->lastDelta();
const bool blocked = _this->BlockPan() || (_this->trackingPOI() && !disable_target_camera_lock);
// The native recognizer owns held-drag movement, including when the legacy Input API does not report the pointer.
// Never replay its delta as momentum.
const bool pointer = PointerHeld() || _this->panningStarted();
bool by_keys = false;
if (native) {
if (!pointer) {
_this->SetLastDelta(blocked ? unity::Vector2{} : d * config.graphics.system_pan_momentum_falloff);
NativePanScalers scalers(_this, pan_speed);
original(_this);
}
_this->SetFarMagRadiusRatioSystemExtended(_this->farMagRadiusRatioSystemNormal());
return true;
}
bool by_keys = false;
// The game's own gate for its shortcuts also covers the tutorial's input lock, modals and a focused text field.
if (keyboard && !native && !blocked && !pointer && ShortcutsManager::CanUseShortcuts()) {
if (keyboard && !blocked && !pointer && ShortcutsManager::CanUseShortcuts()) {
// Opposite bindings cancel; normalize diagonals like the native 2D composite. MoveCamera takes a drag, and
// dragging left moves the view right, hence the negation.
const float x = float(Shortcut::IsPressed(Action::kMoveRight)) - float(Shortcut::IsPressed(Action::kMoveLeft));
Expand All @@ -294,7 +344,8 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this)
d = {-x / length * speed, -y / length * speed};
by_keys = true;
const float scale = _this->deltaScaler();
_this->MoveCamera(d * scale, true);
_this->MoveCamera(d * (scale * pan_speed), true);
d = _this->lastDelta();
}
}

Expand All @@ -304,10 +355,11 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this)
// Glide: the last drag or key delta shrinks by the falloff every frame.
const auto falloff = config.graphics.system_pan_momentum_falloff;
d = d * falloff;
if (!native)
_this->MoveCamera(d, true);
_this->MoveCamera(d, true);
d = _this->lastDelta();
}
_this->SetLastDelta(d);
if (!pointer || blocked)
_this->SetLastDelta(d);
_this->SetFarMagRadiusRatioSystemExtended(_this->farMagRadiusRatioSystemNormal());
return true;
}
Expand Down Expand Up @@ -403,6 +455,8 @@ bool InstallZoom()
bool InstallPan()
{
il2cpp::HookSet hooks("Pan");
IL2CPP_STAGE(hooks, NavigationPan::Class::Method<void(unity::Vector2, bool)>("MoveCamera"),
NavigationPan_MoveCamera_Hook);
IL2CPP_STAGE(hooks, OrbitFrameProvider::Class::Method("UpdateInputData"), OrbitFrameProvider_UpdateInputData_Hook);
IL2CPP_STAGE(hooks, TKTouch::Class::Method("populateWithPosition"), TKTouch_populateWithPosition_Hook);
IL2CPP_STAGE(hooks, NavigationPan::Class::Method("LateUpdate"), NavigationPan_LateUpdate_Hook);
Expand Down
9 changes: 9 additions & 0 deletions src/mod/game/navigation.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ struct NavigationLOD : il2cpp::Object {
{ Call<"UpdateLOD">(level); }
};

enum class NodeDepth : int32_t;

struct NavigationPan : il2cpp::Object {
using Class = il2cpp::Class<"Assembly-CSharp", "Digit.Prime.Navigation", "NavigationPan">;

NodeDepth nodeDepth()
{ return Field<"_nodeDepth", NodeDepth>(); }

unity::Vector2 lastDelta()
{ return Field<"_lastDelta", unity::Vector2>(); }
void SetLastDelta(unity::Vector2 value)
Expand All @@ -38,8 +43,12 @@ struct NavigationPan : il2cpp::Object {
{ return Field<"_panningStarted", bool>(); }
float deltaScaler()
{ return Field<"_deltaScaler", float>(); }
void SetDeltaScaler(float value)
{ SetField<"_deltaScaler">(value); }
float keyboardDeltaScaler()
{ return Field<"_keyboardDeltaScaler", float>(); }
void SetKeyboardDeltaScaler(float value)
{ SetField<"_keyboardDeltaScaler">(value); }
// Managed POI; only checked for null or cleared.
void* trackingPOI()
{ return Field<"_trackingPOI", void*>(); }
Expand Down
41 changes: 41 additions & 0 deletions tests/mod_logic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,46 @@ void RenamedSettings()
Check(both->graphics.ui_scale_step == 0.2f, "the new name wins over the old one");
}

void PanControls()
{
for (const auto* text : {"", "[graphics]\nsystem_pan_speed=0.5", "[graphics]\ngalaxy_pan_speed=2.5",
"[graphics]\nsystem_pan_speed=0.5\ngalaxy_pan_speed=2.5"}) {
toml::table effective;
const auto input = toml::parse(text);
const auto parsed = Config::Parse(input, effective, false);
Check(parsed->graphics.system_pan_speed == (input["graphics"]["system_pan_speed"] ? 0.5f : 1.0f),
"System pan speed keeps its own default and saved value");
Check(parsed->graphics.galaxy_pan_speed == (input["graphics"]["galaxy_pan_speed"] ? 2.5f : 1.0f),
"Galaxy pan speed keeps its own default and saved value");
}
struct Case {
const char* text;
float expected;
};
for (const auto& item : std::array{Case{"0", 0}, Case{"0.8", 0.8f}, Case{"0.99", 0.99f}, Case{"1", 0.99f},
Case{"1.5", 0.99f}, Case{"-0.1", 0}, Case{"nan", 0.8f}, Case{"inf", 0.8f},
Case{"-inf", 0.8f}, Case{"\"bad\"", 0.8f}, Case{"[]", 0.8f}}) {
toml::table effective;
const auto parsed = Config::Parse(
toml::parse(std::string("[graphics]\nsystem_pan_momentum_falloff = ") + item.text), effective, false);
Check(parsed->graphics.system_pan_momentum_falloff == item.expected,
"pan glide saturates finite values below one and defaults unreadable or nonfinite values");
Check(effective["graphics"]["system_pan_momentum_falloff"].value<double>() == double(item.expected),
"runtime vars record the bounded pan glide actually used");
}
for (const auto* key : {"system_pan_speed", "galaxy_pan_speed"})
for (const auto& item : std::array{Case{"0.1", 0.1f}, Case{"0.5", 0.5f}, Case{"1", 1}, Case{"10", 10}, Case{"0", 1},
Case{"0.05", 1}, Case{"1e-40", 1}, Case{"-1", 1}, Case{"10.1", 1},
Case{"nan", 1}, Case{"inf", 1}, Case{"\"bad\"", 1}}) {
toml::table effective;
const auto parsed =
Config::Parse(toml::parse(std::string("[graphics]\n") + key + " = " + item.text), effective, false);
const float value = std::string_view(key) == "system_pan_speed" ? parsed->graphics.system_pan_speed
: parsed->graphics.galaxy_pan_speed;
Check(value == item.expected, "pan speed accepts only finite values in [0.1, 10]");
}
}

void ParseDoesNotActivateSettings()
{
Load("[shortcuts]\nquit = \"F1\"");
Expand Down Expand Up @@ -550,6 +590,7 @@ int main()
DailyFactionSettings();
UpstreamSettings();
RenamedSettings();
PanControls();
ParseDoesNotActivateSettings();
Matching();
ChatStartupGate();
Expand Down
Loading