Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44)
### Interactables Architecture Refactor
- Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc.
- Created `InteractableBase` abstract base class with common functionality that replaces the old component
- Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes
- Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience
### State Machine Integration
- Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements
- Replaced all previous StateMachines by `AppleMachine`
- Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game
- Restores directly to target state without triggering transitional logic
- Migration tool converts existing instances
### Prefab Organization
- Saved changes from scenes into prefabs
- Cleaned up duplicated components, confusing prefabs hierarchies
- Created prefab variants where possible
- Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder
- Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder
- Updated prefab references - All scene references updated to new locations
- Removed placeholder files from Characters, Levels, UI, and Minigames folders
### Scene Updates
- Quarry scene with major updates
- Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD)
- Added proper lighting data
- Updated all interactable components to new architecture
### Minor editor tools
- New tool for testing cards from an editor window (no in-scene object required)
- Updated Interactable Inspector
- New debug option to opt in-and-out of the save/load system
- Tooling for easier migration
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
using UnityEngine ;
using UnityEditor ;
using System.Collections.Generic ;
using System.IO ;
using System.Text.RegularExpressions ;
namespace Editor
{
public class RemoveOldInteractableReferences : EditorWindow
{
private List < string > problematicPrefabs = new List < string > ( ) ;
private Vector2 scrollPosition ;
private bool hasScanned = false ;
2025-11-07 09:32:43 +01:00
[MenuItem("AppleHills/Developer/Remove Old Interactable References")]
Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44)
### Interactables Architecture Refactor
- Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc.
- Created `InteractableBase` abstract base class with common functionality that replaces the old component
- Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes
- Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience
### State Machine Integration
- Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements
- Replaced all previous StateMachines by `AppleMachine`
- Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game
- Restores directly to target state without triggering transitional logic
- Migration tool converts existing instances
### Prefab Organization
- Saved changes from scenes into prefabs
- Cleaned up duplicated components, confusing prefabs hierarchies
- Created prefab variants where possible
- Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder
- Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder
- Updated prefab references - All scene references updated to new locations
- Removed placeholder files from Characters, Levels, UI, and Minigames folders
### Scene Updates
- Quarry scene with major updates
- Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD)
- Added proper lighting data
- Updated all interactable components to new architecture
### Minor editor tools
- New tool for testing cards from an editor window (no in-scene object required)
- Updated Interactable Inspector
- New debug option to opt in-and-out of the save/load system
- Tooling for easier migration
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
public static void ShowWindow ( )
{
var window = GetWindow < RemoveOldInteractableReferences > ( "Clean Old Interactables" ) ;
window . minSize = new Vector2 ( 600 , 400 ) ;
}
private void OnGUI ( )
{
GUILayout . Label ( "Remove Old Interactable/InteractableBase References" , EditorStyles . boldLabel ) ;
EditorGUILayout . HelpBox (
"This tool finds and removes references to:\n" +
"- Interactable (old script name)\n" +
"- InteractableBase (abstract class - not allowed on prefabs)\n\n" +
"These should be replaced by concrete types: Pickup, ItemSlot, or OneClickInteraction" ,
MessageType . Info ) ;
EditorGUILayout . Space ( ) ;
if ( GUILayout . Button ( "Scan All Prefabs" , GUILayout . Height ( 30 ) ) )
{
ScanPrefabs ( ) ;
}
EditorGUILayout . Space ( ) ;
if ( hasScanned )
{
EditorGUILayout . LabelField ( $"Found {problematicPrefabs.Count} prefabs with old references" , EditorStyles . boldLabel ) ;
if ( problematicPrefabs . Count > 0 )
{
EditorGUILayout . Space ( ) ;
if ( GUILayout . Button ( "Clean All Prefabs" , GUILayout . Height ( 30 ) ) )
{
CleanAllPrefabs ( ) ;
}
EditorGUILayout . Space ( ) ;
scrollPosition = EditorGUILayout . BeginScrollView ( scrollPosition ) ;
foreach ( var prefabPath in problematicPrefabs )
{
EditorGUILayout . BeginHorizontal ( "box" ) ;
EditorGUILayout . LabelField ( prefabPath ) ;
if ( GUILayout . Button ( "Clean This" , GUILayout . Width ( 80 ) ) )
{
CleanSinglePrefab ( prefabPath ) ;
}
EditorGUILayout . EndHorizontal ( ) ;
}
EditorGUILayout . EndScrollView ( ) ;
}
else
{
EditorGUILayout . HelpBox ( "No problematic prefabs found! All clean." , MessageType . Info ) ;
}
}
}
private void ScanPrefabs ( )
{
problematicPrefabs . Clear ( ) ;
hasScanned = true ;
string [ ] prefabGuids = AssetDatabase . FindAssets ( "t:Prefab" , new [ ] { "Assets" } ) ;
EditorUtility . DisplayProgressBar ( "Scanning Prefabs" , "Starting..." , 0f ) ;
for ( int i = 0 ; i < prefabGuids . Length ; i + + )
{
string path = AssetDatabase . GUIDToAssetPath ( prefabGuids [ i ] ) ;
EditorUtility . DisplayProgressBar ( "Scanning Prefabs" ,
$"Checking {i + 1}/{prefabGuids.Length}: {Path.GetFileName(path)}" ,
( float ) i / prefabGuids . Length ) ;
if ( PrefabHasOldInteractableReference ( path ) )
{
problematicPrefabs . Add ( path ) ;
}
}
EditorUtility . ClearProgressBar ( ) ;
Debug . Log ( $"<color=cyan>[Scan Complete]</color> Found {problematicPrefabs.Count} prefabs with old Interactable/InteractableBase references." ) ;
}
private bool PrefabHasOldInteractableReference ( string assetPath )
{
try
{
string fullPath = Path . GetFullPath ( assetPath ) ;
string content = File . ReadAllText ( fullPath ) ;
// Look for GUID of Interactable script (11500000 is MonoBehaviour type)
// We're looking for the script reference pattern in YAML
// Pattern: m_Script: {fileID: 11500000, guid: SCRIPT_GUID, type: 3}
// Check if content contains "Interactable" class name references
// This is a simple text search - if the YAML contains these class names, it likely references them
if ( content . Contains ( "InteractableBase" ) | |
( content . Contains ( "Interactable" ) & & ! content . Contains ( "OneClickInteraction" ) ) )
{
// Additional check: Look for MonoBehaviour blocks with missing scripts (fileID: 0)
if ( Regex . IsMatch ( content , @"m_Script:\s*\{fileID:\s*0\}" ) )
{
return true ;
}
// Check for direct class name matches in script references
if ( Regex . IsMatch ( content , @"m_Name:\s*(Interactable|InteractableBase)" ) )
{
return true ;
}
}
return false ;
}
catch ( System . Exception ex )
{
Debug . LogWarning ( $"Error scanning {assetPath}: {ex.Message}" ) ;
return false ;
}
}
private void CleanAllPrefabs ( )
{
if ( ! EditorUtility . DisplayDialog ( "Confirm Cleanup" ,
$"This will remove old Interactable/InteractableBase references from {problematicPrefabs.Count} prefabs.\n\nThis cannot be undone (unless you use version control).\n\nContinue?" ,
"Yes, Clean" , "Cancel" ) )
{
return ;
}
int cleanedCount = 0 ;
for ( int i = 0 ; i < problematicPrefabs . Count ; i + + )
{
string path = problematicPrefabs [ i ] ;
EditorUtility . DisplayProgressBar ( "Cleaning Prefabs" ,
$"Cleaning {i + 1}/{problematicPrefabs.Count}: {Path.GetFileName(path)}" ,
( float ) i / problematicPrefabs . Count ) ;
if ( CleanPrefabFile ( path ) )
{
cleanedCount + + ;
}
}
EditorUtility . ClearProgressBar ( ) ;
AssetDatabase . Refresh ( ) ;
Debug . Log ( $"<color=green>[Cleanup Complete]</color> Cleaned {cleanedCount} prefabs." ) ;
// Re-scan to update the list
ScanPrefabs ( ) ;
}
private void CleanSinglePrefab ( string assetPath )
{
if ( CleanPrefabFile ( assetPath ) )
{
Debug . Log ( $"<color=green>[Cleaned]</color> {assetPath}" ) ;
AssetDatabase . Refresh ( ) ;
// Re-scan to update the list
ScanPrefabs ( ) ;
}
}
private bool CleanPrefabFile ( string assetPath )
{
try
{
string fullPath = Path . GetFullPath ( assetPath ) ;
string content = File . ReadAllText ( fullPath ) ;
string originalContent = content ;
// Pattern 1: Remove entire MonoBehaviour component blocks with missing scripts (fileID: 0)
// This removes the component header and all its properties until the next component or end
string missingScriptPattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Script: \{fileID: 0\}(?:\r?\n(?!---).+)*" ;
content = Regex . Replace ( content , missingScriptPattern , "" , RegexOptions . Multiline ) ;
// Pattern 2: Remove MonoBehaviour blocks that explicitly reference InteractableBase or Interactable
// This is more aggressive and targets the class name directly
string interactablePattern = @"--- !u!114 &\d+\r?\nMonoBehaviour:(?:\r?\n(?!---).+)*?\r?\n m_Name: (?:Interactable|InteractableBase)(?:\r?\n(?!---).+)*" ;
content = Regex . Replace ( content , interactablePattern , "" , RegexOptions . Multiline ) ;
if ( content ! = originalContent )
{
// Clean up any double blank lines that might have been created
content = Regex . Replace ( content , @"(\r?\n){3,}" , "\n\n" ) ;
File . WriteAllText ( fullPath , content ) ;
return true ;
}
return false ;
}
catch ( System . Exception ex )
{
Debug . LogError ( $"Error cleaning {assetPath}: {ex.Message}" ) ;
return false ;
}
}
}
}