Core Modding Concepts
Now that you have your tools set up and understand the branch differences, let's dive into the fundamental concepts that make Schedule 1 modding possible. These concepts form the foundation for all your modding projects.
Unity GameObjects and Components
Schedule 1 is built on Unity, so understanding Unity's core architecture is essential.
GameObject Hierarchy
Every element in Schedule 1 is a GameObject in a scene hierarchy:
// Finding GameObjects in the scene
GameObject playerObject = GameObject.Find("Player");
GameObject[] allEnemies = GameObject.FindGameObjectsWithTag("Enemy");
// Navigating the hierarchy
Transform parent = playerObject.transform.parent;
Transform firstChild = playerObject.transform.GetChild(0);
Component-Based Architecture
GameObjects get functionality through Components:
// Accessing components
Player controller = playerObject.GetComponent<Player>();
Rigidbody physics = playerObject.GetComponent<Rigidbody>();
// Adding/removing components at runtime
playerObject.AddComponent<CustomComponent>();
Destroy(playerObject.GetComponent<OldComponent>());
If you're new to Unity, spend time understanding GameObjects, Components, and the Scene hierarchy. These concepts are crucial for effective modding.
MelonLoader Framework
MelonLoader provides the foundation for all Schedule 1 mods.
Mod Lifecycle
Understanding when your code runs is crucial:
public class MyMod : MelonMod
{
// Called once when the application starts
public override void OnInitializeApplication()
{
LoggerInstance.Msg("Application starting...");
}
// Called when a scene loads
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
LoggerInstance.Msg($"Scene loaded: {sceneName}");
SetupSceneSpecificFeatures();
}
// Called every frame
public override void OnUpdate()
{
// Use sparingly - runs 60+ times per second!
HandleInputChecks();
}
// Called when the application quits
public override void OnApplicationQuit()
{
SaveModSettings();
}
}
Mod Information and Metadata
Properly configure your mod's identity:
using MelonLoader;
// Assembly attributes - define your mod's metadata
[assembly: MelonInfo(typeof(MySchedule1Mod), "My Mod Name", "1.0.0", "Your Name")]
[assembly: MelonGame("Schedule1", "Schedule1")] // Game identification
public class MySchedule1Mod : MelonMod
{
// Mod properties
public override void OnInitializeApplication()
{
LoggerInstance.Msg($"Loading {Info.Name} v{Info.Version}");
LoggerInstance.Msg($"Author: {Info.Author}");
}
}
Harmony Patching System
Harmony allows you to modify existing game methods without changing the original files.
Basic Patching Concepts
Prefix Patches
Run before the original method:
[HarmonyPatch(typeof(PlayerController), "TakeDamage")]
public class TakeDamagePatch
{
// Prefix: runs before TakeDamage
static bool Prefix(PlayerController __instance, float damage)
{
// __instance refers to the object the method was called on
LoggerInstance.Msg($"Player taking {damage} damage");
// Return false to skip the original method
// Return true to continue with original method
return true;
}
}
Postfix Patches
Run after the original method:
[HarmonyPatch(typeof(GameManager), "SaveGame")]
public class SaveGamePatch
{
// Postfix: runs after SaveGame completes
static void Postfix(bool __result)
{
// __result contains the return value of the original method
if (__result)
{
LoggerInstance.Msg("Game saved successfully!");
// Trigger custom save events
OnGameSaved?.Invoke();
}
}
}
Finding Patch Targets
Identifying the right methods and classes to patch is crucial for successful modding. Three main tools help analyze the game's code structure:
dnSpy - Load Schedule1_Data/Managed/Assembly-CSharp.dll to browse class structure, analyze method signatures, and examine IL code. Use search (Ctrl+Shift+K) to find specific methods, look for virtual methods, and use the "Analyze" feature to see method references.
UnityExplorer - Install as a mod to inspect game objects and components in real-time. Browse scene hierarchy, examine attached scripts, and test method calls to understand behavior before patching.
AssetRipper - Extract C# scripts from game assets for readable source code. Locate .cs files in the output to analyze actual implementations and identify optimal patch points.
Combined Approach - Start with AssetRipper for codebase overview, use dnSpy for assembly details, then verify findings with UnityExplorer. Always verify method signatures across tools, test calls before patching, and document your discoveries.
Scene Management
Schedule 1 loads different scenes for menus, gameplay, etc.
Scene Detection and Handling
public class SceneManager : MelonMod
{
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
switch (sceneName.ToLower())
{
case "mainmenu":
SetupMainMenuMods();
break;
case "gameplay":
SetupGameplayMods();
break;
case "settings":
SetupSettingsMods();
break;
}
}
private void SetupGameplayMods()
{
// Find and modify gameplay-specific objects
var hudManager = GameObject.Find("HUDManager");
if (hudManager != null)
{
AddCustomHudElements(hudManager);
}
}
}
Scene Object Discovery
public static class SceneUtils
{
public static T FindObjectOfType<T>() where T : UnityEngine.Object
{
return UnityEngine.Object.FindObjectOfType<T>();
}
public static GameObject FindGameObjectByPath(string path)
{
// Find objects using hierarchy path
var pathParts = path.Split('/');
GameObject current = null;
foreach (var part in pathParts)
{
if (current == null)
current = GameObject.Find(part);
else
current = current.transform.Find(part)?.gameObject;
if (current == null) break;
}
return current;
}
}
Event Systems and Hooks
Schedule 1 uses various event systems for game logic.
Unity Events
public class EventListener : MelonMod
{
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
// Find and hook into Unity Events
var button = GameObject.Find("StartButton")?.GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnStartButtonClicked);
}
}
private void OnStartButtonClicked()
{
LoggerInstance.Msg("Start button was clicked!");
// Add custom logic here
}
}
Custom Event Systems
Create your own event systems for mod communication:
public static class ModEvents
{
public static event Action<Player> OnPlayerSpawned;
public static event Action<float> OnHealthChanged;
public static event Action<string> OnGameStateChanged;
// Trigger events from your patches
public static void TriggerPlayerSpawned(Player player)
{
OnPlayerSpawned?.Invoke(player);
}
}
// Other mods can subscribe to your events
public class OtherMod : MelonMod
{
public override void OnInitializeApplication()
{
ModEvents.OnPlayerSpawned += HandlePlayerSpawned;
}
private void HandlePlayerSpawned(Player player)
{
// React to player spawning
}
}
Configuration and Persistence
Store mod settings and data between game sessions.
MelonPreferences System
public class ModConfig : MelonMod
{
// Define preference categories and entries
private MelonPreferences_Category configCategory;
private MelonPreferences_Entry<bool> enableFeature;
private MelonPreferences_Entry<float> difficultyMultiplier;
public override void OnInitializeApplication()
{
// Create preference category
configCategory = MelonPreferences.CreateCategory("MyMod");
// Create preference entries
enableFeature = configCategory.CreateEntry("EnableFeature", true, "Enable mod feature");
difficultyMultiplier = configCategory.CreateEntry("DifficultyMultiplier", 1.0f, "Difficulty multiplier");
}
// Access configuration values
public bool IsFeatureEnabled => enableFeature.Value;
public float DifficultyMultiplier => difficultyMultiplier.Value;
}
JSON Configuration Files
For complex configurations:
using Newtonsoft.Json;
public class ComplexConfig
{
public Dictionary<string, float> DifficultySettings { get; set; }
public List<string> EnabledFeatures { get; set; }
public CustomSettings Advanced { get; set; }
}
public class ConfigManager
{
private static string ConfigPath => Path.Combine(
MelonUtils.UserDataDirectory, "MyMod", "config.json");
public static ComplexConfig LoadConfig()
{
if (File.Exists(ConfigPath))
{
var json = File.ReadAllText(ConfigPath);
return JsonConvert.DeserializeObject<ComplexConfig>(json);
}
// Return default config
return CreateDefaultConfig();
}
public static void SaveConfig(ComplexConfig config)
{
Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath));
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(ConfigPath, json);
}
}
Error Handling and Debugging
Robust error handling is crucial for stable mods.
Safe Patching
[HarmonyPatch(typeof(GameManager), "CriticalMethod")]
public class SafePatch
{
static bool Prefix()
{
try
{
// Your mod logic here
DoModStuff();
return true;
}
catch (Exception ex)
{
MelonLogger.Error($"Error in mod patch: {ex.Message}");
MelonLogger.Error($"Stack trace: {ex.StackTrace}");
// Return true to let the original method run
return true;
}
}
}
Logging Best Practices
public static class ModLogger
{
private static MelonLogger.Instance logger;
public static void Initialize(MelonLogger.Instance loggerInstance)
{
logger = loggerInstance;
}
public static void Info(string message) => logger?.Msg($"[INFO] {message}");
public static void Warning(string message) => logger?.Warning($"[WARN] {message}");
public static void Error(string message) => logger?.Error($"[ERROR] {message}");
public static void Debug(string message)
{
#if DEBUG
logger?.Msg($"[DEBUG] {message}");
#endif
}
}
Performance Considerations
Efficient Update Patterns
public class PerformantMod : MelonMod
{
private float updateTimer = 0f;
private const float UPDATE_INTERVAL = 0.1f; // Update every 100ms instead of every frame
public override void OnUpdate()
{
updateTimer += Time.deltaTime;
if (updateTimer >= UPDATE_INTERVAL)
{
updateTimer = 0f;
DoPeriodicUpdate();
}
}
private void DoPeriodicUpdate()
{
// Less frequent updates for non-critical operations
}
}
Object Caching
public static class GameObjectCache
{
private static readonly Dictionary<string, GameObject> cache = new();
public static GameObject GetCachedGameObject(string name)
{
if (cache.TryGetValue(name, out var cached) && cached != null)
return cached;
var found = GameObject.Find(name);
if (found != null)
cache[name] = found;
return found;
}
public static void ClearCache()
{
cache.Clear();
}
}
Next Steps
You now understand the core concepts of Schedule 1 modding! These fundamentals will support all your future modding projects.
As you progress, you'll apply these concepts to create:
- UI modifications using Unity's UI system
- Gameplay changes through Harmony patches
- Data persistence with configuration systems
- Complex interactions between game systems
Practice these concepts with simple mods before tackling more complex projects. Each concept builds on the others to create powerful modding capabilities.
Master these core concepts and you'll be well-equipped to tackle any Schedule 1 modding challenge!