//#define THROW_SIMANTICS #if !Server #define IDE_COMPAT #endif using System; using System.Collections.Generic; using System.Linq; using FSO.Content; using FSO.Files.Formats.IFF.Chunks; using FSO.SimAntics.Primitives; using FSO.SimAntics.Model; using FSO.SimAntics.Marshals.Threads; using FSO.SimAntics.Model.TSOPlatform; using FSO.SimAntics.NetPlay.EODs.Model; using System.Threading; namespace FSO.SimAntics.Engine { /// /// Handles instruction execution /// public class VMThread { public static int MAX_USER_ACTIONS = 20; public VMContext Context; private VMEntity Entity; public VMThreadBreakMode ThreadBreak = VMThreadBreakMode.Active; public string ThreadBreakString; public int BreakFrame; //frame the last breakpoint was performed on public bool RoutineDirty; //check tree only vars public bool IsCheck; public List ActionStrings; public Dictionary MotiveAdChanges; public List Stack; private bool ContinueExecution; public List Queue; public VMQueuedAction ActiveAction { get { return (ActiveQueueBlock>-1)?Queue[ActiveQueueBlock]:null; } } /// /// Set when a change to the queue or an item's priority is changed. Internal functions set this, but since you can modify the queue /// from other classes MAKE SURE you set this when such a change is made. (eg. priority set from VMAvatar) /// public bool QueueDirty; public sbyte ActiveQueueBlock = -1; //cannot reorder items in the queue with index <= this. public short[] TempRegisters = new short[20]; public int[] TempXL = new int[2]; public VMPrimitiveExitCode LastStackExitCode = VMPrimitiveExitCode.GOTO_FALSE; public VMAsyncState BlockingState; public VMEODPluginThreadState EODConnection; public bool Interrupt; private ushort ActionUID; // Exception handling variables // Don't need to be serialized. public int DialogCooldown = 0; // the number of ticks that have executed so far this frame. If this exceeds the allowed max, // the thread resets, and a SimAntics Error pops up. public int TicksThisFrame = 0; // the maximum number of primitives a thread can execute in one frame. Tweak appropriately. // variables for internal scheduler public uint ScheduleIdleStart; // keep track of tick when we started idling for an object. must be synced! public uint ScheduleIdleEnd; public static readonly int MAX_LOOP_COUNT = 500000; public static VMPrimitiveExitCode EvaluateCheck(VMContext context, VMEntity entity, VMStackFrame initFrame) { return EvaluateCheck(context, entity, initFrame, null, null); } public static VMPrimitiveExitCode EvaluateCheck(VMContext context, VMEntity entity, VMStackFrame initFrame, VMQueuedAction action) { return EvaluateCheck(context, entity, initFrame, action, null); } public static VMPrimitiveExitCode EvaluateCheck(VMContext context, VMEntity entity, VMStackFrame initFrame, VMQueuedAction action, List actionStrings) { var temp = new VMThread(context, entity, 5); var forceClone = !context.VM.Scheduler.RunningNow; //temps should only persist on check trees running within the vm tick to avoid desyncs. if (entity.Thread != null) { temp.TempRegisters = forceClone?(short[])entity.Thread.TempRegisters.Clone() : entity.Thread.TempRegisters; temp.TempXL = forceClone ? (int[])entity.Thread.TempXL.Clone() : entity.Thread.TempXL; } temp.IsCheck = true; temp.ActionStrings = actionStrings; //generate and place action strings in here temp.Push(initFrame); if (action != null) { temp.Queue.Add(action); //this check runs an action. We may need its interaction number, etc. temp.ActiveQueueBlock = 0; } while (temp.Stack.Count > 0 && temp.DialogCooldown == 0 && !temp.Entity.Dead) //keep going till we're done! idling is for losers! { temp.Tick(); temp.ThreadBreak = VMThreadBreakMode.Active; //cannot breakpoint in check trees } if (actionStrings != null && actionStrings.Count == 0) { //add an action string containing any modified ads actionStrings.Add(new VMPieMenuInteraction() { MotiveAdChanges = temp.MotiveAdChanges }); } if (context.VM.Aborting) return VMPrimitiveExitCode.ERROR; return (temp.DialogCooldown > 0) ? VMPrimitiveExitCode.RETURN_FALSE : temp.LastStackExitCode; } public bool RunInMyStack(VMRoutine routine, GameObject CodeOwner, short[] passVars, VMEntity stackObj) { //a little bit hacky. We may not need to do as serious a context switch as this. var OldStack = Stack; var OldQueue = Queue; var OldCheck = IsCheck; var OldQueueBlock = ActiveQueueBlock; VMStackFrame prevFrame = new VMStackFrame() { Caller = Entity, Callee = Entity }; if (Stack.Count > 0) { prevFrame = Stack[Stack.Count - 1]; Stack = new List() { prevFrame }; } else { Stack = new List(); } Queue = new List(); if (Queue.Count > 0) Queue.Add(Queue[0]); IsCheck = true; ExecuteSubRoutine(prevFrame, routine, CodeOwner, new VMSubRoutineOperand(passVars)); Stack.RemoveAt(0); if (Stack.Count == 0) { Stack = OldStack; Queue = OldQueue; return false; //bhav was invalid/empty } var frame = Stack[Stack.Count - 1]; frame.Callee = stackObj; frame.StackObject = stackObj; try { while (Stack.Count > 0) { NextInstruction(); } } catch (Exception e) { if (e is ThreadAbortException) throw e; //we need to catch these so that the parent can be restored. } //copy child stack things to parent stack Stack = OldStack; Queue = OldQueue; IsCheck = OldCheck; ActiveQueueBlock = OldQueueBlock; return (LastStackExitCode == VMPrimitiveExitCode.RETURN_TRUE) ? true : false; } public VMThread(VMContext context, VMEntity entity, int stackSize) { this.Context = context; this.Entity = entity; this.Stack = new List(stackSize); this.Queue = new List(); } /// /// Checks to see if it can push an interaction, and pushes it. /// Returns true on success, false on failure. /// public bool AttemptPush() { int priorityCompare = ((VMAvatar)Entity).GetPersonData(VMPersonDataVariable.Priority); if (priorityCompare <= 2) priorityCompare -= 1; //autonomous interactions fall through to other ones QueueDirty = true; while (Queue.Count > ActiveQueueBlock+1) { var item = Queue[ActiveQueueBlock+1]; if (item.Priority <= priorityCompare) return false; if (item.NotifyIdle) Entity.SetFlag(VMEntityFlags.InteractionCanceled, item.NotifyIdle); if (IsCheck || ((item.Mode != VMQueueMode.ParentIdle || !Entity.GetFlag(VMEntityFlags.InteractionCanceled)) && CheckAction(item) != null)) { Entity.SetFlag(VMEntityFlags.InteractionCanceled, false); if (!ExecuteAction(item)) return false; ActiveQueueBlock++; return true; } else { Queue.RemoveAt(ActiveQueueBlock + 1); //keep going. } } return false; } public void TryRunImmediately() { //check if we have a run immediately interaction, and inject it if we do.# while (true) { var ind = Queue.FindIndex(x => (x.Flags & TTABFlags.RunImmediately) > 0); if ((ind > ActiveQueueBlock || (!(Stack.Count > 0 && Stack.LastOrDefault().ActionTree) && ind > -1))) { //not already running (if no action we are still not running if we're queue[0], so go for it) //swap current item with ind. var temp = Queue[ind]; Queue.RemoveAt(ind); if (CheckAction(temp) != null) { Queue.Insert(ActiveQueueBlock+1, temp); var frame = temp.ToStackFrame(Entity); frame.SpecialResult = VMSpecialResult.Interaction; Push(frame); ActiveQueueBlock++; //both the run immediately interaction and the active interaction must be protected. break; } } else { break; } } } private void EndCurrentInteraction() { QueueDirty = true; var interaction = Queue[ActiveQueueBlock]; //clear "interaction cancelled" since we are leaving the interaction if (interaction.Mode != VMQueueMode.ParentIdle) Entity.SetFlag(VMEntityFlags.InteractionCanceled, false); if (interaction.Callback != null) interaction.Callback.Run(Entity); if (Queue.Count > 0) Queue.RemoveAt(ActiveQueueBlock); if (Entity is VMAvatar && !IsCheck && ActiveQueueBlock == 0) { //some things are reset when an interaction ends //motive deltas reset between interactions ((VMAvatar)Entity).SetPersonData(VMPersonDataVariable.NonInterruptable, 0); //verified in ts1 ((VMAvatar)Entity).ClearMotiveChanges(); } ContinueExecution = true; //continue where the Allow Push idle left off ActiveQueueBlock--; //update priority with the priority of the interaction we are going back to (or 0) ((VMAvatar)Entity).SetPersonData(VMPersonDataVariable.Priority, (ActiveQueueBlock > -1) ? Queue[ActiveQueueBlock].Priority : (short)0); EvaluateQueuePriorities(); } public void AbortCurrentInteraction() { //go all the way back to the stack frame that Allow Push'd us. var returnTo = Stack.FindLast(x => x.SpecialResult == VMSpecialResult.Interaction); if (returnTo != null) { var ind = Stack.IndexOf(returnTo); while (Stack.Count > ind) { Stack.RemoveAt(Stack.Count-1); } EndCurrentInteraction(); } } public void Tick(){ #if IDE_COMPAT if (ThreadBreak == VMThreadBreakMode.Pause) return; else if (ThreadBreak == VMThreadBreakMode.Reset) { Entity.Reset(Context); ThreadBreak = VMThreadBreakMode.Active; } else if (ThreadBreak == VMThreadBreakMode.Immediate) { Breakpoint(Stack.LastOrDefault(), "Paused."); return; } #endif if (BlockingState != null) BlockingState.WaitTime++; if (DialogCooldown > 0) DialogCooldown--; #if !THROW_SIMANTICS try { #endif if (!Entity.Dead) { if (QueueDirty) { EvaluateQueuePriorities(); TryRunImmediately(); QueueDirty = false; } if (Stack.Count == 0) { if (IsCheck) return; //running out of execution means check trees have ended. Entity.ExecuteEntryPoint(1, Context, false); if (Stack.Count == 0) return; } if ((!Stack.LastOrDefault().ActionTree) || (!Queue[0].Callee.Dead)) //main or our target is not dead { #if IDE_COMPAT if (ThreadBreak == VMThreadBreakMode.ReturnTrue) { var bf = Stack[BreakFrame]; HandleResult(bf, bf.GetCurrentInstruction(), VMPrimitiveExitCode.RETURN_TRUE); Breakpoint(Stack.LastOrDefault(), "Returned True."); return; } if (ThreadBreak == VMThreadBreakMode.ReturnFalse) { var bf = Stack[BreakFrame]; HandleResult(bf, bf.GetCurrentInstruction(), VMPrimitiveExitCode.RETURN_TRUE); Breakpoint(Stack.LastOrDefault(), "Returned False."); return; } #endif ContinueExecution = true; while (ContinueExecution) { if (TicksThisFrame++ > MAX_LOOP_COUNT) { TicksThisFrame = 0; throw new Exception("Thread entered infinite loop! ( >" + MAX_LOOP_COUNT + " primitives)"); } ContinueExecution = false; NextInstruction(); } } else //interaction owner is dead, rip { Entity.Reset(Context); } } #if !THROW_SIMANTICS } catch (Exception e) { #if IDE_COMPAT if (!IsCheck && VM.SignalBreaks) { Breakpoint(Stack.LastOrDefault(), "!"+e.Message+" "+StackTraceSimplify(e.StackTrace.Split('\n').FirstOrDefault(x => x.Contains(".cs")) ?? "")); ContinueExecution = false; return; } #endif if (e is ThreadAbortException) throw e; if (Stack.Count == 0) return; var context = Stack[Stack.Count - 1]; bool Delete = ((Entity is VMGameObject) && (DialogCooldown > 30 * 20 - 10)); if (DialogCooldown == 0) { var simExcept = new VMSimanticsException(e.Message + StackTraceSimplify(e.StackTrace.Split('\n').FirstOrDefault(x => x.Contains(".cs")) ?? ""), context); string exceptionStr = "A SimAntics Exception has occurred, and has been suppressed: \r\n\r\n" + simExcept.ToString() + "\r\n\r\nThe object will be reset. Please report this!"; VMDialogInfo info = new VMDialogInfo { Caller = null, Icon = context.Callee, Operand = new VMDialogOperand { }, Message = exceptionStr, Title = "SimAntics Exception!" }; Context.VM.SignalDialog(info); DialogCooldown = 30 * 20; } if (!IsCheck) { context.Callee.Reset(context.VM.Context); context.Caller.Reset(context.VM.Context); if (Delete) Entity.Delete(true, context.VM.Context); } else { Stack.Clear(); } } #endif } private string StackTraceSimplify(string st) { var lastSlash = st.LastIndexOf('\\'); if (lastSlash == -1) lastSlash = st.LastIndexOf('/'); return (lastSlash == -1) ? st : st.Substring(lastSlash+1); } private void EvaluateQueuePriorities() { EnsureDirectControlAction(); if (ActiveQueueBlock == -1 || ActiveQueueBlock >= Queue.Count) return; var active = Queue[ActiveQueueBlock]; int CurrentPriority = (int)((VMAvatar)Entity).GetPersonData(VMPersonDataVariable.Priority); if (CurrentPriority == (int)VMQueuePriority.Autonomous) CurrentPriority -= 1; // allow other auto actions to interrupt us // HACK: TS1 pushes a "Cancel Interaction" action onto the tree to interrupt itself, as well as setting current interaction to prio 0. // We're simulating that by notifiying idle if priority hits 0. (implied cancel interaction queued) // Make sure this interaction is not *meant* to be priority 0, like tso's idle. active.NotifyIdle = active.Priority != 0 && CurrentPriority == 0; for (int i = ActiveQueueBlock + 1; i < Queue.Count; i++) { if (Queue[i].Callee == null || Queue[i].Callee.Dead) { Queue.RemoveAt(i--); //remove interactions to dead objects (not within active queue block) continue; } if ((int)Queue[i].Priority > CurrentPriority)// && mode != VMQueueMode.ParentIdle) { active.NotifyIdle = true; Entity.SetFlag(VMEntityFlags.InteractionCanceled, true); } } } private void NextInstruction() { /** Next instruction **/ var currentFrame = Stack.LastOrDefault(); if (currentFrame == null) return; if (currentFrame is VMRoutingFrame) HandleResult(currentFrame, null, ((VMRoutingFrame)currentFrame).Tick()); else if (currentFrame is VMDirectControlFrame) HandleResult(currentFrame, null, ((VMDirectControlFrame)currentFrame).Tick()); else { VMInstruction instruction; VMPrimitiveExitCode result = currentFrame.Routine.Execute(currentFrame, out instruction); HandleResult(currentFrame, instruction, result); } } public VMRoutingFrame PushNewRoutingFrame(VMStackFrame frame, bool failureTrees) { var childFrame = new VMRoutingFrame { Routine = frame.Routine, Caller = frame.Caller, Callee = frame.Callee, CodeOwner = frame.CodeOwner, StackObject = frame.StackObject, Thread = this, CallFailureTrees = failureTrees, ActionTree = frame.ActionTree }; Stack.Add(childFrame); return childFrame; } public VMDirectControlFrame PushNewDirectControlFrame(VMStackFrame frame) { var childFrame = new VMDirectControlFrame { Routine = frame.Routine, Caller = frame.Caller, Callee = frame.Callee, CodeOwner = frame.CodeOwner, StackObject = frame.StackObject, Thread = this, ActionTree = frame.ActionTree }; Stack.Add(childFrame); return childFrame; } public void ExecuteSubRoutine(VMStackFrame frame, VMRoutine routine, GameObject codeOwner, VMSubRoutineOperand args) { if (routine == null) { Pop(VMPrimitiveExitCode.ERROR); return; } var childFrame = new VMStackFrame { Routine = routine, Caller = frame.Caller, Callee = frame.Callee, CodeOwner = codeOwner, StackObject = frame.StackObject, _StackObjectID = frame.StackObjectID, //pass this without doing a lookup ActionTree = frame.ActionTree }; childFrame.Args = new short[(routine.Arguments > 4) ? routine.Arguments : 4]; for (var i = 0; i < childFrame.Args.Length; i++) { short argValue = (i > 3) ? (short)-1 : args.Arguments[i]; if (argValue == -1 && args.UseTemp0) { argValue = TempRegisters[i]; } childFrame.Args[i] = argValue; } Push(childFrame); } public VMPrimitiveExitCode ExecuteSubRoutine(VMStackFrame frame, ushort opcode, VMSubRoutineOperand operand) { VMRoutine bhav = null; GameObject CodeOwner; if (opcode >= 8192) { // Semi-Global sub-routine call bhav = (VMRoutine)frame.ScopeResource.SemiGlobal.GetRoutine(opcode); } else if (opcode >= 4096) { // Private sub-routine call bhav = (VMRoutine)frame.ScopeResource.GetRoutine(opcode); } else { // Global sub-routine call //CodeOwner = frame.Global.Resource; bhav = (VMRoutine)frame.Global.Resource.GetRoutine(opcode); } CodeOwner = frame.CodeOwner; ExecuteSubRoutine(frame, bhav, CodeOwner, operand); #if IDE_COMPAT if (Stack.LastOrDefault().GetCurrentInstruction().Breakpoint || ThreadBreak == VMThreadBreakMode.StepIn) { Breakpoint(frame, "Stepped in."); ContinueExecution = false; } else #endif { ContinueExecution = true; } return VMPrimitiveExitCode.CONTINUE; } private void ExecuteInstruction(VMStackFrame frame) { var instruction = frame.GetCurrentInstruction(); var opcode = instruction.Opcode; if (opcode >= 256) { ExecuteSubRoutine(frame, opcode, (VMSubRoutineOperand)instruction.Operand); return; } var primitive = VMContext.Primitives[opcode]; if (primitive == null) { HandleResult(frame, instruction, VMPrimitiveExitCode.GOTO_TRUE); return; } VMPrimitiveHandler handler = primitive.GetHandler(); var result = handler.Execute(frame, instruction.Operand); HandleResult(frame, instruction, result); } private void HandleResult(VMStackFrame frame, VMInstruction instruction, VMPrimitiveExitCode result) { switch (result) { // Don't advance the instruction pointer, this primitive isnt finished yet case VMPrimitiveExitCode.CONTINUE_NEXT_TICK: ScheduleIdleStart = Context.VM.Scheduler.CurrentTickID; Context.VM.Scheduler.ScheduleTickIn(Entity, 1); ContinueExecution = false; break; case VMPrimitiveExitCode.CONTINUE_FUTURE_TICK: ContinueExecution = false; break; case VMPrimitiveExitCode.ERROR: ContinueExecution = false; Pop(result); break; case VMPrimitiveExitCode.RETURN_TRUE: case VMPrimitiveExitCode.RETURN_FALSE: /** pop stack and return false **/ Pop(result); break; case VMPrimitiveExitCode.GOTO_TRUE: MoveToInstruction(frame, instruction.TruePointer, true); break; case VMPrimitiveExitCode.GOTO_FALSE: MoveToInstruction(frame, instruction.FalsePointer, true); break; case VMPrimitiveExitCode.GOTO_TRUE_NEXT_TICK: MoveToInstruction(frame, instruction.TruePointer, true); if (ContinueExecution) { ScheduleIdleStart = Context.VM.Scheduler.CurrentTickID; Context.VM.Scheduler.ScheduleTickIn(Entity, 1); ContinueExecution = false; } break; case VMPrimitiveExitCode.GOTO_FALSE_NEXT_TICK: MoveToInstruction(frame, instruction.FalsePointer, true); if (ContinueExecution) { ScheduleIdleStart = Context.VM.Scheduler.CurrentTickID; Context.VM.Scheduler.ScheduleTickIn(Entity, 1); ContinueExecution = false; } break; case VMPrimitiveExitCode.CONTINUE: ContinueExecution = true; break; case VMPrimitiveExitCode.INTERRUPT: Stack.Clear(); QueueDirty = true; if (Queue.Count > 0) Queue.RemoveAt(0); LastStackExitCode = result; break; } } private void MoveToInstruction(VMStackFrame frame, byte instruction, bool continueExecution) { if (frame is VMRoutingFrame) { //TODO: Handle returning false into the pathfinder (indicates failure) return; } ContinueExecution = continueExecution; switch (instruction) { case 255: Pop(VMPrimitiveExitCode.RETURN_FALSE); break; case 254: Pop(VMPrimitiveExitCode.RETURN_TRUE); break; case 253: //attempt to continue along only path available. if (frame.GetCurrentInstruction().TruePointer != 253) { MoveToInstruction(frame, frame.GetCurrentInstruction().TruePointer, continueExecution); return; } else if (frame.GetCurrentInstruction().FalsePointer != 253) { MoveToInstruction(frame, frame.GetCurrentInstruction().FalsePointer, continueExecution); return; } Pop(VMPrimitiveExitCode.ERROR); break; default: frame.InstructionPointer = instruction; if (frame.GetCurrentInstruction().Breakpoint || (ThreadBreak != VMThreadBreakMode.Active && ( ThreadBreak == VMThreadBreakMode.StepIn || (ThreadBreak == VMThreadBreakMode.StepOver && Stack.Count - 1 <= BreakFrame) || (ThreadBreak == VMThreadBreakMode.StepOut && Stack.Count <= BreakFrame) ))) { string result = "Unknown Break"; switch (ThreadBreak) { case VMThreadBreakMode.StepIn: result = "Stepped In."; break; case VMThreadBreakMode.StepOut: result = "Stepped Out."; break; case VMThreadBreakMode.StepOver: result = "Stepped Over."; break; } Breakpoint(frame, result); } break; } ContinueExecution = (ThreadBreak != VMThreadBreakMode.Pause) && ContinueExecution; } public void Breakpoint(VMStackFrame frame, string description) { if (IsCheck) return; //can't breakpoint in check trees. ThreadBreak = VMThreadBreakMode.Pause; ThreadBreakString = description; BreakFrame = Stack.IndexOf(frame); Context.VM.BreakpointHit(Entity); } public void Pop(VMPrimitiveExitCode result) { var discardResult = Stack[Stack.Count - 1].SpecialResult; var contextSwitch = (Stack.Count > 1) && Stack.LastOrDefault().ActionTree != Stack[Stack.Count - 2].ActionTree; Stack.RemoveAt(Stack.Count - 1); LastStackExitCode = result; if (discardResult == VMSpecialResult.Interaction) //interaction switching back to main (it cannot be the other way...) { var interaction = Queue[ActiveQueueBlock]; EndCurrentInteraction(); result = (!interaction.Flags.HasFlag(TTABFlags.RunImmediately)) ? VMPrimitiveExitCode.CONTINUE_NEXT_TICK : VMPrimitiveExitCode.CONTINUE; } else if (discardResult == VMSpecialResult.Retry) { result = VMPrimitiveExitCode.CONTINUE; } if (Stack.Count > 0) { if (result == VMPrimitiveExitCode.RETURN_TRUE) result = VMPrimitiveExitCode.GOTO_TRUE; else if (result == VMPrimitiveExitCode.RETURN_FALSE) result = VMPrimitiveExitCode.GOTO_FALSE; var currentFrame = Stack.Last(); HandleResult(currentFrame, currentFrame.GetCurrentInstruction(), result); } else // :( { ContinueExecution = false; } } public bool Push(VMStackFrame frame) { if (frame.Routine.Instructions.Length == 0) return false; //some bhavs are empty... do not execute these. Stack.Add(frame); /** Initialize the locals **/ var numLocals = Math.Max(frame.Routine.Locals, frame.Routine.Arguments); frame.Locals = new short[numLocals]; frame.Thread = this; frame.InstructionPointer = 0; return true; } /// /// Add an item to the action queue /// /// public void EnqueueAction(VMQueuedAction invocation) { invocation.UID = ActionUID++; QueueDirty = true; if (!IsCheck && (invocation.Flags & TTABFlags.RunImmediately) > 0) { // shove this action in the queue to try run it next tick. // interaction can be run normally if we actually hit it using allow push. (unlikely) // otherwise next tick will detect the "run immediately" interaction's presence. // and it will be pushed to the stack immediately. // TODO: check if any interactions of this kind clobber the temps. // this doesnt ""run immediately"", but is good enough. invocation.Mode = VMQueueMode.Idle; //hide //invocation.Priority = short.MinValue; this.Queue.Add(invocation); return; } var leapfrog = Context.VM.TS1 ? (TTABFlags)0: TTABFlags.Leapfrog; if (Queue.Count == 0) //if empty, just queue right at the front this.Queue.Add(invocation); else if ((invocation.Flags & TTABFlags.FSOPushHead) > 0) //place right after active interaction, ignoring all priorities. this.Queue.Insert(ActiveQueueBlock + 1, invocation); else if ((invocation.Flags & (TTABFlags.FSOPushTail | leapfrog)) > 0 && invocation.Mode != VMQueueMode.ParentExit) { //this one's weird. start at the left til there's a lower priority. (eg. parent exit or idle) bool hitParentEnd = (invocation.Mode != VMQueueMode.ParentIdle); for (int i = ActiveQueueBlock+1; i < Queue.Count; i++) { if (Queue[i].Priority < invocation.Priority) //we have higher priority than this item { this.Queue.Insert(i, invocation); //insert before this. will come before parent exit and pals. EvaluateQueuePriorities(); return; } } this.Queue.Add(invocation); //right at the end! (somehow) } else //we've got an even harder job! find a place for this interaction based on its priority { bool hitParentEnd = (invocation.Mode != VMQueueMode.ParentIdle); for (int i = Queue.Count - 1; i > ActiveQueueBlock; i--) { if (hitParentEnd && (invocation.Priority <= Queue[i].Priority || Queue[i].Mode == VMQueueMode.ParentExit)) //skip until we find a parent exit or something with the same or higher priority. { this.Queue.Insert(i + 1, invocation); if (Context.VM.TS1 && invocation.Priority <= Queue[i].Priority) { // queue skip all items with a lower priority (after this interaction) // i've verified this happens in ts1, but I don't know if it does in TSO so i've locked it for now. i += 2; while (i < this.Queue.Count) { CancelAction(this.Queue[i].UID); } } EvaluateQueuePriorities(); return; } if (Queue[i].Mode == VMQueueMode.ParentExit) hitParentEnd = true; } this.Queue.Insert(ActiveQueueBlock + 1, invocation); //this is more important than all other queued items that are not running, so stick this to run next. } EvaluateQueuePriorities(); } public void CancelAction(ushort actionUID) { var interaction = Queue.FirstOrDefault(x => x.UID == actionUID); if (interaction != null) { if (Entity is VMAvatar && interaction == Queue[0] && Context.VM.EODHost != null) Context.VM.EODHost.ForceDisconnect((VMAvatar)Entity); QueueDirty = true; interaction.NotifyIdle = true; //cancel any idle parents after this interaction var index = Queue.IndexOf(interaction); if (interaction.Mode == Engine.VMQueueMode.ParentIdle) { for (int i = index + 1; i < Queue.Count; i++) { if (Queue[i].Mode == Engine.VMQueueMode.ParentIdle) { if (interaction.Mode == Engine.VMQueueMode.ParentIdle) Queue.RemoveAt(i--); else { Queue[i].NotifyIdle = true; Queue[i].Priority = 0; } } else if (Queue[i].Mode == Engine.VMQueueMode.ParentExit) { Queue[i].NotifyIdle = true; Queue[i].Priority = 0; } //parent exit needs to "appear" like it is cancelled. } } var canQueueSkip = !interaction.Flags.HasFlag(TTABFlags.MustRun); if (canQueueSkip && (index > ActiveQueueBlock || Stack.LastOrDefault()?.ActionTree == false) && (interaction.Mode == Engine.VMQueueMode.Normal || interaction.Flags.HasFlag(TTABFlags.FSODirectControl))) { Queue.Remove(interaction); if (Context.VM.TS1) interaction.Callee.ExecuteEntryPoint(4, Context, true, Entity); //queue skipped } else { Entity.SetFlag(VMEntityFlags.InteractionCanceled, true); ((VMAvatar)Entity).SetPersonData(VMPersonDataVariable.Priority, 0); } } } private bool ExecuteAction(VMQueuedAction action) { //set the new interaction's priority ((VMAvatar)Entity).SetPersonData(VMPersonDataVariable.Priority, action.Priority); var frame = action.ToStackFrame(Entity); frame.SpecialResult = VMSpecialResult.Interaction; return Push(frame); } public List CheckTS1Action(VMQueuedAction action, bool auto) { var result = new List(); if (Entity is VMAvatar && !action.Flags.HasFlag(TTABFlags.FSOSkipPermissions)) //just let everyone use the CSR interactions { var avatar = (VMAvatar)Entity; if ((action.Flags & (TTABFlags.TS1AllowCats | TTABFlags.TS1AllowDogs)) > 0) { //interaction can only be performed by cats or dogs //if (!avatar.IsPet) return null; //check we're the correct type if (avatar.IsCat && (action.Flags & TTABFlags.TS1AllowCats) == 0) return null; if (avatar.IsDog && (action.Flags & TTABFlags.TS1AllowDogs) == 0) return null; } else if (avatar.IsPet) return null; //not allowed var isVisitor = avatar.GetPersonData(VMPersonDataVariable.PersonType) == 1 && avatar.GetPersonData(VMPersonDataVariable.GreetStatus) < 2; //avatar.ObjectID != Context.VM.GetGlobalValue(3); var debugTrees = false; TTABFlags ts1State = ((isVisitor) ? TTABFlags.AllowVisitors : 0) | ((avatar.GetPersonData(VMPersonDataVariable.PersonsAge) < 18) ? TTABFlags.TS1NoChild : 0) | ((avatar.GetPersonData(VMPersonDataVariable.PersonsAge) >= 18 && !avatar.IsPet) ? TTABFlags.TS1NoAdult : 0); //DEBUG: enable debug interction for all CSRs. if ((action.Flags & TTABFlags.Debug) > 0) { if (!isVisitor && debugTrees) return result; //do not bother running check else return null; //disable debug for everyone else. } //NEGATIVE EFFECTS: var pos = ts1State & (TTABFlags.TS1NoChild | TTABFlags.TS1NoAdult); var ts1Compare = action.Flags; if ((pos & ts1Compare) > 0) return null; var negMask = (TTABFlags.AllowVisitors); var negatedFlags = (~ts1Compare) & negMask; if ((negatedFlags & ts1State) > 0) return null; //we are disallowed } if (action.CheckRoutine != null) { var args = new short[4]; if (auto) args[0] = 1; if (EvaluateCheck(Context, Entity, new VMStackFrame() { Caller = Entity, Callee = action.Callee, CodeOwner = action.CodeOwner, StackObject = action.StackObject, Routine = action.CheckRoutine, Args = args }, null, result) != VMPrimitiveExitCode.RETURN_TRUE) { return null; } } return result; } public List CheckAction(VMQueuedAction action, bool auto = false) { // 1. check action flags for permissions (if we are avatar) // 2. run check tree // rules: // Dogs/Cats means people CANNOT use these interactions. (DogsFlag|CatsFlag & IsDog|IsCat) // When Allow Object Owner is OFF it disallows the object owner, otherwise no effect // Visitors, Roommates, Ghosts have this same negative effect. // Friends apprars to override Owner, Visitors, Roommates // Allow CSRs:positive effect. if (action == null) return null; if (Context.VM.TS1) return CheckTS1Action(action, auto); var result = new List(); if (!action.Flags.HasFlag(TTABFlags.FSOSkipPermissions) && Entity is VMAvatar) //just let everyone use the CSR interactions { var avatar = (VMAvatar)Entity; if (avatar.GetSlot(0) != null && (action.Flags & TTABFlags.TSOAvailableCarrying) == 0) return null; if ((action.Flags & (TTABFlags.AllowCats | TTABFlags.AllowDogs)) > 0) { //interaction can only be performed by cats or dogs if (!avatar.IsPet) return null; //check we're the correct type if (avatar.IsCat && (action.Flags & TTABFlags.AllowCats) == 0) return null; if (avatar.IsDog && (action.Flags & TTABFlags.AllowDogs) == 0) return null; } else if (avatar.IsPet && (avatar.AvatarState.Permissions < VMTSOAvatarPermissions.Admin || auto)) return null; //not allowed bool isActionGlobal = action.ActionRoutine.ID < 4096; // Ignore global actions for disabling interactions due to repair. bool isRepair = (action.Flags & TTABFlags.TSOIsRepair) > 0; if ((!isActionGlobal || isRepair) && (isRepair != ((action.Callee.MultitileGroup.BaseObject?.TSOState as VMTSOObjectState)?.Broken ?? false))) return null; uint ownerID = 0; if (action.Callee is VMGameObject) { var state = ((VMTSOObjectState)action.Callee.TSOState); ownerID = state?.OwnerID ?? 0; if (ownerID != 0 && state.ObjectFlags.HasFlag(VMTSOObjectFlags.FSODonated)) { ownerID = Context.VM.TSOState.OwnerID; //owner rewrite to mayor } } TSOFlags tsoState = ((!(action.Callee is VMGameObject) || avatar.PersistID == ownerID) ? TSOFlags.AllowObjectOwner : 0) | ((avatar.AvatarState.Permissions == VMTSOAvatarPermissions.Visitor) ? TSOFlags.AllowVisitors : 0) | ((avatar.AvatarState.Permissions >= VMTSOAvatarPermissions.Roommate) ? TSOFlags.AllowRoommates : 0) | ((avatar.AvatarState.Permissions == VMTSOAvatarPermissions.Admin) ? TSOFlags.AllowCSRs : 0) | ((avatar.GetPersonData(VMPersonDataVariable.IsGhost) > 0) ? TSOFlags.AllowGhost : 0) | TSOFlags.AllowFriends; TSOFlags tsoCompare = action.Flags2; //if flags are empty apart from "Non-Empty", force everything but visitor. (a kind of default state) if (tsoCompare == TSOFlags.NonEmpty) tsoCompare |= TSOFlags.AllowFriends | TSOFlags.AllowRoommates | TSOFlags.AllowObjectOwner; //DEBUG: enable debug interction for all CSRs. if ((action.Flags & TTABFlags.Debug) > 0) { if ((tsoState & TSOFlags.AllowCSRs) > 0) return result; //do not bother running check else return null; //disable debug for everyone else. } if ((action.Flags & TTABFlags.TSOAvailableWhenDead) > 0) tsoCompare |= TSOFlags.AllowGhost; if ((action.Flags & TTABFlags.AllowVisitors) > 0) tsoCompare |= TSOFlags.AllowVisitors; //wrong??????? var posMask = (TSOFlags.AllowObjectOwner); if (((tsoState & posMask) & (tsoCompare & posMask)) == 0) { //NEGATIVE EFFECTS: var negMask = (TSOFlags.AllowVisitors | TSOFlags.AllowRoommates | TSOFlags.AllowGhost); var negatedFlags = (~tsoCompare) & negMask; if ((negatedFlags & tsoState) > 0) return null; //we are disallowed if ((tsoCompare & TSOFlags.AllowCSRs) > 0 && (tsoState & TSOFlags.AllowCSRs) == 0) return null; // only admins can run csr. } } if ((!action.Flags.HasFlag(TTABFlags.FSOSkipPermissions) || ((action.Flags & TTABFlags.TSORunCheckAlways) > 0)) && action.CheckRoutine != null) { var args = new short[4]; if (auto) args[0] = 1; if (EvaluateCheck(Context, Entity, new VMStackFrame() { Caller = Entity, Callee = action.Callee, CodeOwner = action.CodeOwner, StackObject = action.StackObject, Routine = action.CheckRoutine, Args = args }, null, result) != VMPrimitiveExitCode.RETURN_TRUE) { return null; } } return result; } public void EnsureDirectControlAction() { if (!(Entity is VMAvatar ava) || VM.GlobTS1 || ava.GetPersonData(VMPersonDataVariable.UnusedAndDoNotUse2) != 32767 || ava.GetPersonData(VMPersonDataVariable.Posture) != 0 || Queue.Any(entry => entry.Flags.HasFlag(TTABFlags.FSODirectControl))) { return; } var routine = Entity.GetRoutineWithOwner(9001, Context); if (routine == null || routine.routine == null) return; EnqueueAction( new VMQueuedAction { Callee = Entity, CodeOwner = routine.owner, ActionRoutine = routine.routine, Name = "Direct Control", StackObject = Entity, Args = new short[4], Priority = (short)VMQueuePriority.Idle + 1, Flags = TTABFlags.FSOSkipPermissions | TTABFlags.FSODirectControl, Mode = VMQueueMode.Idle } ); } #region VM Marshalling Functions public virtual VMThreadMarshal Save() { var stack = new VMStackFrameMarshal[Stack.Count]; int i = 0; foreach (var item in Stack) stack[i++] = item.Save(); var queue = new VMQueuedActionMarshal[Queue.Count]; i = 0; foreach (var item in Queue) queue[i++] = item.Save(); return new VMThreadMarshal { Stack = stack, Queue = queue, ActiveQueueBlock = ActiveQueueBlock, TempRegisters = (short[])TempRegisters.Clone(), TempXL = (int[])TempXL.Clone(), LastStackExitCode = LastStackExitCode, BlockingState = BlockingState, EODConnection = EODConnection, Interrupt = Interrupt, ActionUID = ActionUID, DialogCooldown = DialogCooldown, ScheduleIdleStart = ScheduleIdleStart }; } public virtual void Load(VMThreadMarshal input, VMContext context) { Stack = new List(); foreach (var item in input.Stack) { if (item is VMRoutingFrameMarshal) { Stack.Add(new VMRoutingFrame(item, context, this)); } else if (item is VMDirectControlFrameMarshal) { Stack.Add(new VMDirectControlFrame(item, context, this)); } else { Stack.Add(new VMStackFrame(item, context, this)); } } Queue = new List(); QueueDirty = true; foreach (var item in input.Queue) Queue.Add(new VMQueuedAction(item, context)); ActiveQueueBlock = input.ActiveQueueBlock; TempRegisters = input.TempRegisters; TempXL = input.TempXL; LastStackExitCode = input.LastStackExitCode; BlockingState = input.BlockingState; EODConnection = input.EODConnection; Interrupt = input.Interrupt; ActionUID = input.ActionUID; DialogCooldown = input.DialogCooldown; ScheduleIdleStart = input.ScheduleIdleStart; } public VMThread(VMThreadMarshal input, VMContext context, VMEntity entity) { Context = context; Entity = entity; Load(input, context); } #endregion } public enum VMThreadBreakMode { Active = 0, Pause = 1, StepIn = 2, StepOut = 3, StepOver = 4, ReturnTrue = 5, ReturnFalse = 6, Immediate = 7, Reset = 8 } }