private void UpdateTalkingHeadSeek(VM vm, VMAvatar talker) { // Update head seek of everyone else to attempt to look at the person talking. var channel = vm.TSOState.ChatChannels.FirstOrDefault(x => x.ID == ChannelID); if (ChannelID == 7) { channel = VMTSOChatChannel.AdminChannel; } if (channel != null && channel.ViewPermMin != VMTSOAvatarPermissions.Visitor) { return; // Cannot use look towards on private channels. } bool isImportantChannel = channel != null && channel.SendPermMin > VMTSOAvatarPermissions.Visitor && channel.Flags.HasFlag(VMTSOChatChannelFlags.EnableTTS); int multiplier = isImportantChannel ? 2 : 1; int talkerRoom = vm.Context.GetObjectRoom(talker); foreach (VMAvatar avatar in vm.Context.ObjectQueries.Avatars) { if (avatar == talker) { continue; } if (!isImportantChannel) { // Check if the avatar is in the same room, and rather close by. int avatarRoom = vm.Context.GetObjectRoom(avatar); if (avatarRoom != talkerRoom || LotTilePos.Distance(avatar.Position, talker.Position) > 16 * 10) { continue; // Not close enough. } } var avatarHeadTarget = vm.GetObjectById(avatar.GetPersonData(VMPersonDataVariable.HeadSeekObject)); var avatarHeadFinish = avatar.GetPersonData(VMPersonDataVariable.HeadSeekFinishAction); var avatarHeadState = avatar.GetPersonData(VMPersonDataVariable.HeadSeekState); if (avatarHeadState == 8 || avatarHeadState == 0 || (avatarHeadTarget is VMAvatar && (avatarHeadFinish != -1 || isImportantChannel))) { // We can look towards the talker. (important talkers have priority) avatar.SetPersonData(VMPersonDataVariable.HeadSeekObject, talker.ObjectID); avatar.SetPersonData(VMPersonDataVariable.HeadSeekState, 1); //in progress flag only avatar.SetPersonData(VMPersonDataVariable.HeadSeekLimitAction, 1); //look back on limit? avatar.SetPersonData(VMPersonDataVariable.HeadSeekFinishAction, (short)(isImportantChannel ? -1 : 0)); //use to store if the person was an important talker avatar.SetPersonData(VMPersonDataVariable.HeadSeekTimeout, (short)(talker.MessageTimeout * multiplier)); //while the message is present } } }
// TODO: float values may desync if devices are not both x86 or using a different C# library. // Might need to replace with fixed point library for position and rotation /// <summary> /// This method will find all the avaliable locations within the criteria ordered by proximity to the optimal proximity /// External functions can then decide which is most desirable. E.g. the nearest slot to the object may be the longest route if /// its in another room. /// </summary> /// <param name="obj"></param> /// <param name="slot"></param> /// <param name="context"></param> /// <returns></returns> public List <VMFindLocationResult> FindAvaliableLocations(VMEntity obj, VMContext context, VMEntity caller) { /** * Start at min proximity and circle around the object to find the avaliable locations. * Then pick the one nearest to the optimal value */ /** * ------ MAJOR TODO: ------ * Avoid vector math at all costs! Small differences in hardware could cause desyncs. * This really goes for all areas of the SimAntics engine, but here it's particularly bad. */ Vector2 center; if (OnlySit) { FailCode = VMRouteFailCode.NoChair; } // if we need to use the average location of an object group, it needs to be calculated. if (((Flags & SLOTFlags.UseAverageObjectLocation) > 0) && (obj.MultitileGroup.MultiTile)) { center = new Vector2(0, 0); var objs = obj.MultitileGroup.Objects; for (int i = 0; i < objs.Count; i++) { center += new Vector2(objs[i].Position.x / 16f, objs[i].Position.y / 16f); } center /= objs.Count; } else { center = new Vector2(obj.Position.x / 16f, obj.Position.y / 16f); } //add offset of slot if it exists. must be rotated to be relative to object var rotOff = Vector3.Transform(Slot.Offset, Matrix.CreateRotationZ(obj.RadianDirection)); var circleCtr = new Vector2(center.X + rotOff.X / 16, center.Y + rotOff.Y / 16); ushort room = context.VM.Context.GetRoomAt(obj.Position); Results = new List <VMFindLocationResult>(); if ((Flags & SLOTFlags.SnapToDirection) > 0) { //snap to the specified direction, on the specified point. double baseRot; if (Slot.Facing > SLOTFacing.FaceAwayFromObject) { // bit of a legacy thing here. Facing field did not use to exist, // which is why SnapToDirection was hacked to use the directional flags. // now that it exists, it is used instead, to encode the same information... // just transform back into old format. Flags |= (SLOTFlags)(1 << (int)Slot.Facing); } else { if (((int)Flags & 255) == 0) { Flags |= SLOTFlags.NORTH; } } var flagRot = DirectionUtils.PosMod(obj.RadianDirection + FlagsAsRad(Flags), Math.PI * 2); if (flagRot > Math.PI) { flagRot -= Math.PI * 2; } VerifyAndAddLocation(obj, circleCtr, center, Flags, Double.MaxValue, context, caller, (float)flagRot); return(Results); } else { if (((int)Flags & 255) == 0 || Slot.Offset != new Vector3()) { //exact position //Flags |= (SLOTFlags)255; // special case, walk directly to point. VerifyAndAddLocation(obj, circleCtr, center, Flags, Double.MaxValue, context, caller, float.NaN); return(Results); } var maxScore = Math.Max(DesiredProximity - MinProximity, MaxProximity - DesiredProximity) + (LotTilePos.Distance(obj.Position, caller.Position) + MaxProximity) / 3 + 2; var ignoreRooms = (Flags & SLOTFlags.IgnoreRooms) > 0; SLOTEnumerationFunction((x, y, distance) => { var pos = new Vector2(circleCtr.X + x / 16.0f, circleCtr.Y + y / 16.0f); if (distance >= MinProximity - 0.5 && distance <= MaxProximity + 0.5 && (ignoreRooms || context.VM.Context.GetRoomAt(new LotTilePos((short)Math.Round(pos.X * 16), (short)Math.Round(pos.Y * 16), obj.Position.Level)) == room)) //slot is within proximity { var routeEntryFlags = (GetSearchDirection(circleCtr, pos, obj.RadianDirection) & Flags); //the route needs to know what conditions it fulfilled if (routeEntryFlags > 0) //within search location { double baseScore = ((maxScore - Math.Abs(DesiredProximity - distance)) + context.VM.Context.NextRandom(1024) / 1024.0f); VerifyAndAddLocation(obj, pos, center, routeEntryFlags, baseScore, context, caller, float.NaN); } } }); } /** Sort by how close they are to desired proximity **/ if (Results.Count > 1) { Results = Results.OrderBy(x => - x.Score).ToList(); //avoid sort because it acts incredibly unusually } if (Results.Count > 0) { FailCode = VMRouteFailCode.Success; } return(Results); }
private void VerifyAndAddLocation(VMEntity obj, Vector2 pos, Vector2 center, SLOTFlags entryFlags, double score, VMContext context, VMEntity caller, float facingDir) { //note: verification is not performed if snap target slot is enabled. var tpos = new LotTilePos((short)Math.Round(pos.X * 16), (short)Math.Round(pos.Y * 16), obj.Position.Level); if (context.IsOutOfBounds(tpos)) { return; } score -= LotTilePos.Distance(tpos, caller.Position) / 3.0; if (Slot.SnapTargetSlot < 0 && context.Architecture.RaycastWall(new Point((int)pos.X, (int)pos.Y), new Point(obj.Position.TileX, obj.Position.TileY), obj.Position.Level)) { SetFail(VMRouteFailCode.WallInWay, null); return; } bool faceAnywhere = false; if (float.IsNaN(facingDir)) { var obj3P = obj.Position.ToVector3(); var objP = new Vector2(obj3P.X, obj3P.Y); switch (Slot.Facing) { case SLOTFacing.FaceTowardsObject: facingDir = (float)GetDirectionTo(pos, objP); break; case SLOTFacing.FaceAwayFromObject: facingDir = (float)GetDirectionTo(objP, pos); break; case SLOTFacing.FaceAnywhere: faceAnywhere = true; facingDir = 0.0f; break; default: int intDir = (int)Math.Round(Math.Log((double)obj.Direction, 2)); var rotatedF = ((int)Slot.Facing + intDir) % 8; facingDir = (float)(((int)rotatedF > 4) ? ((double)rotatedF * Math.PI / 4.0) : (((double)rotatedF - 8.0) * Math.PI / 4.0)); break; } } VMFindLocationResult result = new VMFindLocationResult { Position = new LotTilePos((short)Math.Round(pos.X * 16), (short)Math.Round(pos.Y * 16), obj.Position.Level), RadianDirection = facingDir, FaceAnywhere = faceAnywhere, RouteEntryFlags = entryFlags }; var avatarInWay = false; if (Slot.SnapTargetSlot < 0) { var solid = caller.PositionValid(tpos, Direction.NORTH, context, VMPlaceRequestFlags.AcceptSlots); if (solid.Status != Model.VMPlacementError.Success) { if (solid.Object != null) { if (solid.Object is VMGameObject) { if (Slot.Sitting > 0 && solid.Object.EntryPoints[26].ActionFunction != 0) { result.Chair = solid.Object; } else { SetFail(VMRouteFailCode.DestTileOccupied, solid.Object); return; } } else { avatarInWay = true; } } } if (context.ObjectQueries.GetObjectsAt(tpos)?.Any( x => ((VMEntityFlags2)x.GetValue(VMStackObjectVariable.FlagField2) & VMEntityFlags2.ArchitectualDoor) > 0) ?? false) { avatarInWay = true; //prefer not standing in front of a door. (todo: merge with above check?) } if (result.Chair != null && (Math.Abs(DirectionUtils.Difference(result.Chair.RadianDirection, facingDir)) > Math.PI / 4)) { return; //not a valid goal. } if (result.Chair == null && OnlySit) { return; } score = score * ((result.Chair != null) ? Slot.Sitting : Slot.Standing); //if an avatar is in or going to our destination positon, we this spot becomes low priority as getting into it will require a shoo. if (!avatarInWay) { foreach (var avatar in context.ObjectQueries.Avatars) { if (avatar == caller) { continue; } //search for routing frame. is its destination the same as ours? if (avatar.Thread != null) { var intersects = avatar.Thread.Stack.Any(x => x is VMRoutingFrame && ((VMRoutingFrame)x).IntersectsOurDestination(result)); if (intersects) { score = Math.Max(double.Epsilon, score); break; } } } } else { score = Math.Max(double.Epsilon, score); } } result.Score = score; Results.Add(result); }
private void VerifyAndAddLocation(VMEntity obj, Vector2 pos, Vector2 center, SLOTFlags entryFlags, double score, VMContext context, VMEntity caller, float facingDir) { //note: verification is not performed if snap target slot is enabled. var tpos = new LotTilePos((short)Math.Round(pos.X * 16), (short)Math.Round(pos.Y * 16), obj.Position.Level); if (context.IsOutOfBounds(tpos)) { return; } score -= LotTilePos.Distance(tpos, caller.Position) / 3.0; if (Slot.SnapTargetSlot < 0 && context.Architecture.RaycastWall(new Point((int)pos.X, (int)pos.Y), new Point(obj.Position.TileX, obj.Position.TileY), obj.Position.Level)) { SetFail(VMRouteFailCode.WallInWay, null); return; } bool faceAnywhere = false; if (float.IsNaN(facingDir)) { var obj3P = obj.Position.ToVector3(); var objP = new Vector2(obj3P.X, obj3P.Y); switch (Slot.Facing) { case SLOTFacing.FaceTowardsObject: facingDir = (float)GetDirectionTo(pos, objP); break; case SLOTFacing.FaceAwayFromObject: facingDir = (float)GetDirectionTo(objP, pos); break; case SLOTFacing.FaceAnywhere: faceAnywhere = true; facingDir = 0.0f; break; default: int intDir = (int)Math.Round(Math.Log((double)obj.Direction, 2)); var rotatedF = ((int)Slot.Facing + intDir) % 8; facingDir = (float)(((int)rotatedF > 4) ? ((double)rotatedF * Math.PI / 4.0) : (((double)rotatedF - 8.0) * Math.PI / 4.0)); break; } } VMEntity chair = null; if (Slot.SnapTargetSlot < 0) { var solid = caller.PositionValid(tpos, Direction.NORTH, context); if (solid.Status != Model.VMPlacementError.Success) { if (solid.Object != null && solid.Object is VMGameObject) { if (Slot.Sitting > 0 && solid.Object.EntryPoints[26].ActionFunction != 0) { chair = solid.Object; } else { SetFail(VMRouteFailCode.DestTileOccupied, solid.Object); return; } } } if (chair != null && (Math.Abs(DirectionUtils.Difference(chair.RadianDirection, facingDir)) > Math.PI / 4)) { return; //not a valid goal. } if (chair == null && OnlySit) { return; } } Results.Add(new VMFindLocationResult { Position = new LotTilePos((short)Math.Round(pos.X * 16), (short)Math.Round(pos.Y * 16), obj.Position.Level), Score = score * ((chair != null) ? Slot.Sitting : Slot.Standing), //todo: prefer closer? RadianDirection = facingDir, Chair = chair, FaceAnywhere = faceAnywhere, RouteEntryFlags = entryFlags }); }