Skip to content

Instantly share code, notes, and snippets.

@v1ld
Last active October 17, 2024 22:06
Show Gist options
  • Select an option

  • Save v1ld/2f8bd3f71517e0322d7b92596ca8ac05 to your computer and use it in GitHub Desktop.

Select an option

Save v1ld/2f8bd3f71517e0322d7b92596ca8ac05 to your computer and use it in GitHub Desktop.
A short example showing how to use Mod Settings listeners to keep your settings object updated

Mod Settings & Continuous Updates in Cyberpunk 2077

A simple, real-world example to show how to set up your mod to use Mod Settings using annotated redscript code.

// Uses a module for all the usual reasons; no need to call my classes names like V1ldSettings
module V1ld.DontDisassembleExpensiveJunk

// Settings class must derive from ScriptableSystem which then creates and manages the singleton instance used by Mod Settings
class Settings extends ScriptableSystem {
  // Define your mod's settings, annotated by Mod Settings' properties
  @runtimeProperty("ModSettings.mod", "Don't Disassemble Expensive Junk")
  @runtimeProperty("ModSettings.displayName", "Enable mod")
  @runtimeProperty("ModSettings.description", "Enable or disable the mod.")
  let enabled: Bool = true;

  @runtimeProperty("ModSettings.mod", "Don't Disassemble Expensive Junk")
  @runtimeProperty("ModSettings.category", "Features")
  @runtimeProperty("ModSettings.category.order", "1")
  @runtimeProperty("ModSettings.displayName", "Cutoff price")
  @runtimeProperty("ModSettings.description", "All items cheaper than this will be disassembled.")
  @runtimeProperty("ModSettings.step", "10")
  @runtimeProperty("ModSettings.min", "10")
  @runtimeProperty("ModSettings.max", "5000")
  @runtimeProperty("ModSettings.dependency", "enabled")
  let cutoffPrice: Int32 = 50;

  // A simple helper method used by the mod
  public final func ShouldDisassembleItem(player: ref<PlayerPuppet>, itemID: ItemID) -> Bool {
    return this.enabled && RPGManager.CalculateSellPrice(player.GetGame(), player, itemID) < this.cutoffPrice;
  }

  // Mod Settings helpers & listeners

  // A convenience getter for the singleton managed by ScriptableSystem
  public static func Get(gi: GameInstance) -> ref<Settings> {
    // Must use the fully qualified name for this class here: V1ld.DontDisassembleExpensiveJunk.Settings
    return GameInstance.GetScriptableSystemsContainer(gi).Get(n"V1ld.DontDisassembleExpensiveJunk.Settings") as Settings;
  }

  // The OnAttach()/OnDetach() methods of ScriptableSystem are called when a save is loaded and unloaded.
  //
  // We use redscript's @if(ModuleExists()) conditional inclusion here to let the mod work even if the player
  // hasn't installed Mod Settings. The @runtimeProperty declarations above will also be simply ignored if
  // Mod Settings is not installed. The only difference will be that settings can't be changed, the mod
  // will use the default setting defined above.
  @if(ModuleExists("ModSettingsModule"))
  private func OnAttach() -> Void { ModSettings.RegisterListenerToClass(this); }

  @if(ModuleExists("ModSettingsModule"))
  private func OnDetach() -> Void { ModSettings.RegisterListenerToClass(this); }
}

That's it! Your mod is now set up to use Mod Settings and to have its settings values continuously updated as players make changes to their settings.

Here's an example of how to use the above in your mod. This is in the same file as above in my case, but doesn't really matter.

@replaceMethod(BackpackMainGameController)
  protected cb func OnDisassembleJunkPopupClosed(data: ref<inkGameNotificationData>) -> Bool {
    // [...]
      while i < limit {
        // Here we fetch a reference to our settings singleton. Note we can use just "Settings" instead of
        // the fully module qualified name. This works because we're still in the same file and still within
        // scope of the module declaration at the top. If this was another file, we'd need to use the fully
        // qualified name instead V1ld.DontDisassembleExpensiveJunk.Settings.Get()
        let cutoff = Settings.Get(this.m_player.GetGame()); 
        // And here we use it.  it's that simple.
        //
        // You can directly access cutoff.enabled or cutoff.cutoffPrice here too, if you prefer.
        if cutoff.ShouldDisassembleItem(this.m_player, this.m_junkItems[i].GetID()) {
          ItemActionsHelper.DisassembleItem(this.m_player, this.m_junkItems[i].GetID(), this.m_junkItems[i].GetQuantity());
        }
        i += 1;
      }
    // [...]
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment