Code/Systems/VNScript/Script/Script.cs
using Sandbox;
using Sandbox.Diagnostics;
using System;
using System.Linq;
using System.Collections.Generic;
using VNBase;
using VNBase.Assets;
namespace VNScript;
/// <summary>
/// This class contains the dialogue structures as well as the functions to process dialogue and labels from the S-expression code
/// </summary>
public partial class Script
{
public Dictionary<string, Label> Labels { get; } = new();
public Label InitialLabel { get; internal set; } = new();
internal Dictionary<Value, Value> Variables { get; } = new();
private static readonly Logger Log = new( "VNScript" );
/// <summary>
/// Parse a new script from the provided code.
/// </summary>
public static Script ParseScript( List<SParen> codeBlocks )
{
var script = new Script();
script.Parse( codeBlocks );
return script;
}
private void Parse( List<SParen> codeBlocks )
{
var parsingFunctions = CreateParsingFunctions();
foreach ( var sParen in codeBlocks )
{
sParen.Execute( parsingFunctions );
}
}
private EnvironmentMap CreateParsingFunctions()
{
var functionEnvironment = new EnvironmentMap();
var functions = new Dictionary<string, Value.FunctionValue>
{
{ "label", new Value.FunctionValue( CreateLabel ) },
{ "start", new Value.FunctionValue( SetStartDialogue ) },
{ "set", new Value.FunctionValue( SetVariable ) },
{ "defun", new Value.FunctionValue( DefineFunction ) }
};
foreach ( var function in functions )
{
functionEnvironment.SetVariable( function.Key, function.Value );
}
return functionEnvironment;
}
private Value.NoneValue SetVariable( IEnvironment environment, Value[] values )
{
for ( var i = 0; i < values.Length - 1; i += 2 )
{
var key = values[i];
var value = values[i + 1];
Variables[key] = value;
}
return Value.NoneValue.None;
}
private Value.FunctionValue DefineFunction( IEnvironment environment, Value[] values )
{
// Expect: (defun function-name (param1 param2 ...) (body))
if ( values.Length != 3 )
{
throw ParamError.Wrong( "defun", "(defun name (params...) body)", values );
}
// Extract function name
var functionName = values[0] switch
{
Value.VariableReferenceValue varRef => varRef.Name,
Value.StringValue strVal => strVal.Text,
_ => throw ParamError.Wrong( "defun", "function name as first parameter", values )
};
// Extract parameter list
if ( values[1] is not Value.ListValue paramList )
{
throw ParamError.Wrong( "defun", "parameter list as second parameter", values );
}
// Extract body
if ( values[2] is not Value.ListValue body )
{
throw ParamError.Wrong( "defun", "function body as third parameter", values );
}
var argNames = paramList.ValueList.Select( p => p switch
{
Value.StringValue stringValue => stringValue.Text,
Value.VariableReferenceValue variableReferenceValue => variableReferenceValue.Name,
_ => throw new InvalidParametersException( [p] )
} ).ToArray();
// Create the function value
var functionValue = new Value.FunctionValue( ( env, arglist ) =>
{
if ( arglist.Length != argNames.Length )
{
throw new InvalidParametersException( arglist );
}
var functionEnv = new EnvironmentMap( env );
for ( var i = 0; i < argNames.Length; i++ )
{
functionEnv.SetVariable( argNames[i], arglist[i].Evaluate( env ) );
}
body.Deconstruct( out var valueList );
return valueList.Execute( functionEnv );
} );
// Store the function in script variables so it's available at runtime
Variables[new Value.VariableReferenceValue( functionName )] = functionValue;
// Also register in the parsing environment for use during parsing
environment.SetVariable( functionName, functionValue );
return functionValue;
}
private Value.NoneValue SetStartDialogue( IEnvironment environment, Value[] values )
{
InitialLabel = Labels[(values[0] as Value.VariableReferenceValue)!.Name];
return Value.NoneValue.None;
}
private Value.NoneValue CreateLabel( IEnvironment environment, Value[] values )
{
var label = new Label();
var labelName = values[0] switch
{
Value.StringValue stringValue => stringValue.Text,
Value.VariableReferenceValue variableReferenceValue => variableReferenceValue.Name,
_ => throw new InvalidParametersException( [values[0]] )
};
Labels[labelName] = label;
label.Name = labelName;
for ( var i = 1; i < values.Length; i++ )
{
var argument = ((Value.ListValue)values[i]).ValueList;
ProcessLabelArgument( argument, label );
}
return Value.NoneValue.None;
}
private static void ProcessLabelArgument( SParen arguments, Label label )
{
var argumentType = ((Value.VariableReferenceValue)arguments[0]).Name;
LabelArgument? labelArgument = argumentType switch
{
"dialogue" => LabelDialogueArgument,
"choice" => LabelChoiceArgument,
"char" => LabelCharacterArgument,
"sound" => LabelSoundArgument,
"music" => LabelMusicArgument,
"bg" => LabelBackgroundArgument,
"input" => LabelInputArgument,
"after" => LabelAfterArgument,
_ => null // Unknown - treat as executable code block
};
if ( labelArgument != null )
{
labelArgument( arguments, label );
}
else
{
// This is an executable code block (like (if ...), (when ...), (set ...), etc.)
// Store it to be executed when the label becomes active
if ( label.AfterLabel is null )
{
label.AfterLabel = new After();
}
label.AfterLabel.CodeBlocks.Add( arguments );
}
}
private delegate void LabelArgument( SParen argument, Label label );
private delegate int DialogueArgument( SParen argument, int index, Label label, Dialogue dialogue );
private delegate int ChoiceArgument( SParen argument, int index, Choice choice );
private delegate int CharacterArgument( SParen argument, int index, Label label, Character character );
private delegate int SoundArgument( SParen argument, int index, Label label, VNBase.Assets.Sound sound );
private delegate int AfterArgument( SParen argument, int index, After after );
private static void LabelAfterArgument( SParen arguments, Label label )
{
// Don't replace existing AfterLabel - create only if it doesn't exist
if ( label.AfterLabel is null )
{
label.AfterLabel = new After();
}
for ( var i = 1; i < arguments.Count; i++ )
{
switch ( arguments[i] )
{
case Value.ListValue listValue:
label.AfterLabel.CodeBlocks.Add( listValue.ValueList );
break;
case Value.VariableReferenceValue variableReferenceValue:
AfterArgument afterArgument = variableReferenceValue.Name switch
{
"end" => AfterEndDialogueArgument,
"jump" => AfterJumpArgument,
"load" => AfterLoadScriptArgument,
_ => throw new ArgumentOutOfRangeException( variableReferenceValue.Name )
};
i += afterArgument( arguments, i, label.AfterLabel );
break;
default:
throw new InvalidParametersException( [arguments[i]] );
}
}
}
private static int AfterJumpArgument( SParen arguments, int index, After after )
{
var labelName = (arguments[index + 1] as Value.VariableReferenceValue)!.Name;
after.TargetLabel = labelName;
return 1;
}
private static int AfterEndDialogueArgument( SParen arguments, int index, After after )
{
after.IsLastLabel = true;
return 0;
}
private static int AfterLoadScriptArgument( SParen arguments, int index, After after )
{
after.ScriptPath = (arguments[index + 1] as Value.VariableReferenceValue)!.Name;
return 1;
}
private static void LabelChoiceArgument( SParen arguments, Label label )
{
if ( arguments[1] is not Value.StringValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
var choice = new Choice();
label.Choices.Add( choice );
choice.Text = argument.Text;
for ( var i = 2; i < arguments.Count; i++ )
{
if ( arguments[i] is not Value.VariableReferenceValue variableReferenceValue )
{
throw new InvalidParametersException( [arguments[i]] );
}
ChoiceArgument choiceArgument = variableReferenceValue.Name switch
{
"jump" => ChoiceJumpArgument,
"cond" => ChoiceConditionArgument,
_ => throw new ArgumentOutOfRangeException( variableReferenceValue.Name )
};
i += choiceArgument( arguments, i, choice );
}
}
private static int ChoiceConditionArgument( SParen arguments, int index, Choice choice )
{
choice.Condition = (arguments[index + 1] as Value.ListValue)!.ValueList;
return 1;
}
private static int ChoiceJumpArgument( SParen arguments, int index, Choice choice )
{
choice.TargetLabel = (arguments[index + 1] as Value.VariableReferenceValue)!.Name;
return 1;
}
private static void LabelDialogueArgument( SParen arguments, Label label )
{
// Collect all text parts until we hit a keyword or run out of arguments
var textParts = new List<Value>();
int i = 1;
// Gather all the text components (strings, variables, or expressions)
while ( i < arguments.Count )
{
var arg = arguments[i];
// Check if this is a keyword argument (like "speaker")
if ( arg is Value.VariableReferenceValue varRef && IsDialogueKeyword( varRef.Name ) )
{
break;
}
textParts.Add( arg );
i++;
}
// Need at least one text part
if ( textParts.Count == 0 )
{
throw new InvalidParametersException( arguments.ToArray() );
}
// Build the formatted text string
var textBuilder = new System.Text.StringBuilder();
var formattedText = new FormattableText( string.Empty );
foreach ( var part in textParts )
{
switch ( part )
{
case Value.StringValue str:
textBuilder.Append( str.Text );
break;
case Value.VariableReferenceValue varRef:
// Add as a format placeholder: {variableName}
textBuilder.Append( $"{{{varRef.Name}}}" );
break;
case Value.ListValue listVal:
// Add expression placeholder and store the expression
var placeholder = formattedText.AddExpression( listVal.ValueList );
textBuilder.Append( $"{{{placeholder}}}" );
break;
default:
throw new InvalidParametersException( [part] );
}
}
formattedText.Text = textBuilder.ToString();
// Create the dialogue entry with the built text
var entry = new Dialogue
{
Text = formattedText,
Speaker = null
};
// Process any remaining keyword arguments
while ( i < arguments.Count )
{
if ( arguments[i] is not Value.VariableReferenceValue variableReferenceValue )
{
throw new InvalidParametersException( [arguments[i]] );
}
DialogueArgument dialogueArgument = variableReferenceValue.Name switch
{
"speaker" => DialogueSpeakerArgument,
_ => throw new ArgumentOutOfRangeException( variableReferenceValue.Name )
};
i += dialogueArgument( arguments, i, label, entry );
}
label.Dialogues.Add( entry );
}
private static bool IsDialogueKeyword( string name )
{
return name == "speaker";
}
private static int DialogueSpeakerArgument( SParen arguments, int index, Label label, Dialogue dialogue )
{
var characterName = ((Value.VariableReferenceValue)arguments[index + 1]).Name;
var character = GetCharacterResource( characterName ) ?? throw new ResourceNotFoundException( $"Unable to set speaking character, character resource with name {characterName} couldn't be found!", characterName );
dialogue.Speaker = character;
return 1;
}
private static void LabelCharacterArgument( SParen arguments, Label label )
{
var characterName = ((Value.VariableReferenceValue)arguments[1]).Name;
var character = GetCharacterResource( characterName ) ?? throw new ResourceNotFoundException( $"Unable to add character, character resource with name {characterName} couldn't be found!", characterName );
label.Characters.Add( character );
for ( var i = 2; i < arguments.Count; i++ )
{
if ( arguments[i] is not Value.VariableReferenceValue variableReferenceValue )
{
throw new InvalidParametersException( [arguments[i]] );
}
CharacterArgument characterArgument = variableReferenceValue.Name switch
{
"exp" => LabelCharacterExpressionArgument,
_ => throw new ArgumentOutOfRangeException( variableReferenceValue.Name )
};
i += characterArgument( arguments, i, label, character );
}
}
private static int LabelCharacterExpressionArgument( SParen arguments, int index, Label label, Character character )
{
if ( arguments[index + 1] is not Value.VariableReferenceValue argument )
{
throw new InvalidParametersException( [arguments[index + 1]] );
}
character.ActivePortrait = argument.Name;
return 1;
}
private static void LabelSoundArgument( SParen arguments, Label label )
{
if ( arguments[1] is not Value.StringValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
var soundName = argument.Text;
var sound = new VNBase.Assets.Sound( soundName );
label.Assets.Add( sound );
for ( var i = 2; i < arguments.Count; i++ )
{
if ( arguments[i] is not Value.VariableReferenceValue variableReferenceValue )
{
throw new InvalidParametersException( [arguments[i]] );
}
SoundArgument soundArgument = variableReferenceValue.Name switch
{
"mixer" => SoundMixerArgument, _ => throw new ArgumentOutOfRangeException( variableReferenceValue.Name )
};
i += soundArgument( arguments, i, label, sound );
}
}
private static int SoundMixerArgument( SParen arguments, int index, Label label, VNBase.Assets.Sound sound )
{
if ( arguments[index + 1] is not Value.StringValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
sound.MixerName = argument.Text;
return 1;
}
private static void LabelMusicArgument( SParen arguments, Label label )
{
if ( arguments[1] is not Value.StringValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
var musicName = argument.Text;
label.Assets.Add( new Music( musicName ) );
}
private static void LabelBackgroundArgument( SParen arguments, Label label )
{
if ( arguments[1] is not Value.StringValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
var backgroundName = argument.Text;
var backgroundPath = $"{Settings.BackgroundsPath}{backgroundName}";
label.Assets.Add( new Background( backgroundPath ) );
}
private static void LabelInputArgument( SParen arguments, Label label )
{
if ( arguments[1] is not Value.VariableReferenceValue argument )
{
throw new InvalidParametersException( [arguments[1]] );
}
if ( label.Choices.Count > 0 )
{
throw new InvalidOperationException( "Cannot have a text input in a label with choices!" );
}
label.ActiveInput = new Input { VariableName = argument.Name };
}
private static Character? GetCharacterResource( string characterName )
{
var characterPath = $"{Settings.CharacterResourcesPath}{characterName}.char";
return ResourceLibrary.TryGet<Character>( characterPath, out var loadedCharacter ) ? loadedCharacter : null;
}
}