diff --git a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ClientSettings.uc b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ClientSettings.uc index b7c123f..04516a3 100644 --- a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ClientSettings.uc +++ b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ClientSettings.uc @@ -23,9 +23,14 @@ var config Name BookmarkModifierButton; var config Name ZoomButton; var config name BehindViewKey; var config name SwitchViewToButton; +var config name SelectSwitchViewToButton; var config float PlayerSwitchDelay; var config float PostPlayerSwitchDelay; +var config float EventTimeout; + +var config float EventSwitchDelay; +var config float PostEventSwitchDelay; var config string PickupNotificationPattern; var config string RedFlagNotificationPattern, BlueFlagNotificationPattern; @@ -55,7 +60,12 @@ defaultproperties ZoomButton=MiddleMouseButton BehindViewKey=Q SwitchViewToButton=Multiply + SelectSwitchViewToButton=none PlayerSwitchDelay=0.5 PostPlayerSwitchDelay=2.0 + + EventSwitchDelay=1.5 + PostEventSwitchDelay=3.0 + EventTimeout=15.0 } diff --git a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_Interaction.uc b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_Interaction.uc index f5fd0f6..eb78b80 100644 --- a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_Interaction.uc +++ b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_Interaction.uc @@ -25,9 +25,14 @@ enum ESelectionState { SS_PostSelect }; var ESelectionState SelectionInProgress; +var byte SelectionMode; +var bool ForceSelectionClose; var string SelectedPrefix; +const SELECTIONMODE_PLAYERS = 1; +const SELECTIONMODE_EVENTS = 2; + var bool BookmarkModifierButtonHeld; var SpectatorUI_Bookmarks Bookmarks; var array BookmarkKeys; @@ -60,6 +65,25 @@ struct SpectatorUI_RespawnTimer { }; var array RespawnTimers; +// cached events +struct SpectatorUI_EventParm { + var class PickupActors; + var name FlagEventType; + var string Message; +}; +struct SpectatorUI_CachedEvent { + var Actor EventActor; + var PlayerReplicationInfo Instigator; + var SpectatorUI_EventParm EventParameters; + var float Time; + var float Expiring; +}; +var array CachedEvents; +var Actor LastEventActor; +var PlayerReplicationInfo LastEventPRI; + +var array Events; +var int SelectedEventIndex; static function SpectatorUI_Interaction Create(UTPlayerController PC, SpectatorUI_ReplicationInfo newRI) { local SpectatorUI_Interaction SUI_Interaction, OldInteraction; @@ -107,6 +131,7 @@ function Cleanup() { } PRIs.Length = 0; + Events.Length = 0; RI = None; if (ShortManualRef != None) { @@ -115,6 +140,7 @@ function Cleanup() { } RespawnTimers.Length = 0; + CachedEvents.Length = 0; Interactions.RemoveItem(self); } @@ -212,7 +238,8 @@ event PostRender(Canvas Canvas) { RenderPickupTimers(Canvas); if (SelectionInProgress != SS_None) { - RenderPlayerList(Canvas); + if (SelectionMode == SELECTIONMODE_PLAYERS) RenderPlayerList(Canvas); + else if (SelectionMode == SELECTIONMODE_EVENTS) RenderEventList(Canvas); } if (bZoomButtonHeld) { @@ -396,6 +423,12 @@ function bool HandleInputKey(int ControllerId, name Key, EInputEvent EventType, i = SpeedBinds.Find('Key', Key); if (i != INDEX_NONE) { SpectatorCameraSpeed = default.SpectatorCameraSpeed * GetCameraSpeedMultiplier(SpeedBinds[i].Value); + } else if (Key == 'Escape' && SelectionInProgress > SS_None) { + EndSelect(true); + return true; + } else if (Key == 'Enter' && SelectionInProgress == SS_InProgress) { + EndSelect(); + return true; } else if (key == Settings.BookmarkModifierButton) { BookmarkModifierButtonHeld = true; } else if (key == Settings.ZoomButton) { @@ -404,21 +437,29 @@ function bool HandleInputKey(int ControllerId, name Key, EInputEvent EventType, } else { bZoomButtonHeld = true; } + } else if (Settings.SelectSwitchViewToButton != '' && Key == Settings.SelectSwitchViewToButton) { + MaybeViewPointOfInterest(); } else if (Key == Settings.SwitchViewToButton) { - RI.ViewPointOfInterest(); + if (Settings.SelectSwitchViewToButton == '' && (PlayerInput.PressedKeys.Find('LeftAlt') != INDEX_NONE || PlayerInput.PressedKeys.Find('RightAlt') != INDEX_NONE)) + MaybeViewPointOfInterest(); + else + RI.ViewPointOfInterest(); } else if (Key == Settings.BehindViewKey) { BehindView(); } else if (BookmarkKeys.Find(Key) != INDEX_NONE) { BookmarkButtonPressed(Key); } else { if (BindString == "GBA_NextWeapon") { - PlayerSelect(+1); + SelectionNext(true); return true; } else if (BindString == "GBA_PrevWeapon") { - PlayerSelect(-1); + SelectionNext(false); return true; } else if (BindString == "GBA_AltFire") { - if (AnimatedCamera(PlayerCamera) == None) { + if (SelectionInProgress >= SS_InProgress) { + EndSelect(true); + return true; + } else if (AnimatedCamera(PlayerCamera) == None) { GetPlayerViewPoint(Loc, Rot); Rot.Roll = 0; // we don't really want dutch angle, do we? @@ -436,6 +477,9 @@ function bool HandleInputKey(int ControllerId, name Key, EInputEvent EventType, if (ShortManualRef != None) { CloseManual(); return true; + } else if (SelectionInProgress >= SS_InProgress) { + EndSelect(); + return true; } // otherwise, let ViewObjective handle it } else if (ShortManualRef != None) { @@ -470,6 +514,121 @@ function bool HandleInptAxis(int ControllerId, name Key, float Delta, float Delt return false; } +function MaybeViewPointOfInterest() +{ + local Actor A; + local PlayerReplicationInfo PRI; + ClearExpiredEvents(); + + if (CachedEvents.Length == 1) { + if (IsValidEventForSelection(CachedEvents[0])) { + A = CachedEvents[0].EventActor; + PRI = CachedEvents[0].Instigator; + CachedEvents.Length = 0; + } + } else if (CachedEvents.Length > 1) { + EventSelect(); + } else if (LastEventActor != none) { + A = LastEventActor; + PRI = LastEventPRI; + } + + if (A != none) { + LastEventActor = none; + LastEventPRI = none; + RI.SwitchViewToPointOfInterest(A, PRI); + } +} + +function EndSelect(optional bool bAbort = false) +{ + if (bAbort) SelectionInProgress = SS_PostSelect; + ForceSelectionClose = true; + if (SelectionMode == SELECTIONMODE_PLAYERS) EndPlayerSelect(); + else if (SelectionMode == SELECTIONMODE_EVENTS) EndEventSelect(); + ForceSelectionClose = false; +} + +function SelectionNext(bool bNext) +{ + // prioritize event selection if visible + if (SelectionMode == SELECTIONMODE_EVENTS && SelectionInProgress >= SS_InProgress) { + EventSelect(bNext ? +1 : -1); + } else { + PlayerSelect(bNext ? +1 : -1); + } +} + +function bool IsValidEventForSelection(SpectatorUI_CachedEvent evnt) +{ + local array temparray; + if (evnt.EventActor == none) return false; + + // simulated foreach to mimic Find(STRUCT) + temparray = CachedEvents; + temparray.RemoveItem(evnt); + if (temparray.Length == CachedEvents.Length) return false; + + return true; +} + +function EventSelect(optional int increment = 1) +{ + local int i; + local SpectatorUI_CachedEvent evt; + + if (SelectionMode > 0 && SelectionMode != SELECTIONMODE_EVENTS) return; + SelectionMode = SELECTIONMODE_EVENTS; + + if (SelectionInProgress == SS_None) { + SelectionInProgress = SS_InProgress; + Events.Length = 0; + + for (i=0; i 0) { + SetTimer(Settings.EventSwitchDelay, false, 'EndEventSelect', self); + } else { + EndEventSelect(); + } +} + +function EndEventSelect() +{ + ClearTimer('EndEventSelect', self); + if (SelectionInProgress == SS_InProgress) { + SelectionInProgress = SS_PostSelect; + + RI.SwitchViewToPointOfInterest(Events[SelectedEventIndex].EventActor, Events[SelectedEventIndex].Instigator); + if (Settings.PostEventSwitchDelay > 0 && !ForceSelectionClose) { + SetTimer(Settings.PostEventSwitchDelay, false, 'EndEventSelect', self); + } else { + EndEventSelect(); + } + } else if (SelectionInProgress == SS_PostSelect) { + SelectionInProgress = SS_None; + Events.Length = 0; + SelectionMode = 0; + + LastEventActor = none; + LastEventPRI = none; + } +} + static function bool IsValidSpectatorTarget(PlayerReplicationInfo PRI) { if (PRI == None || PRI.bOnlySpectator) return false; @@ -486,6 +645,9 @@ function PlayerSelect(int increment) local UTGameReplicationInfo UTGRI; local int TeamIndex; + if (SelectionMode > 0 && SelectionMode != SELECTIONMODE_PLAYERS) return; + SelectionMode = SELECTIONMODE_PLAYERS; + if (SelectionInProgress == SS_None) { SelectionInProgress = SS_InProgress; PRIs.Length = 0; @@ -522,11 +684,12 @@ function PlayerSelect(int increment) function EndPlayerSelect() { + ClearTimer('EndPlayerSelect', self); if (SelectionInProgress == SS_InProgress) { SelectionInProgress = SS_PostSelect; RI.ViewPlayer(PRIs[SelectedPRIIndex]); - if (Settings.PostPlayerSwitchDelay > 0) { + if (Settings.PostPlayerSwitchDelay > 0 && !ForceSelectionClose) { SetTimer(Settings.PostPlayerSwitchDelay, false, 'EndPlayerSelect', self); } else { EndPlayerSelect(); @@ -534,6 +697,7 @@ function EndPlayerSelect() } else if (SelectionInProgress == SS_PostSelect) { SelectionInProgress = SS_None; PRIs.Length = 0; + SelectionMode = 0; } } @@ -624,6 +788,105 @@ function RenderPlayerList(Canvas C) C.ClipX = OldClipX; } +function string GetEventString(SpectatorUI_CachedEvent evnt, optional out string Extra) { + local string s; + s = evnt.EventParameters.Message; + if (s == "") s = GetPickupName(evnt.EventActor.Class); + if (evnt.Time > 0.0) Extra = " @"$class'UTHUD'.static.FormatTime(evnt.Time); + return s; +} + +function float GetLongestEventListEntry(Canvas C, optional bool IncludingExtra) +{ + local SpectatorUI_CachedEvent evt; + local float XL, YL; + local float Res; + local string s; + foreach Events(evt) { + s = GetEventString(evt, s)$(IncludingExtra ? s : ""); + C.StrLen(s, XL, YL); + if (XL > Res) Res = XL; + } + return Res; +} + +function RenderEventList(Canvas C) +{ + local UTHUD HUD; + local SpectatorUI_CachedEvent evnt; + local int Index; + local LinearColor LC; + local float XL, YL; + local vector2d POS; + local float OldClipX; + local string ExtraString; + HUD = UTHUD(myHUD); + if (HUD == None) return; + + C.Reset(); + C.Font = HUD.GetFontSizeIndex(1); + + C.StrLen(SelectedPrefix, XL, YL); + + // XXX why clock? to be honest, I forgot + POS = HUD.ResolveHudPosition(HUD.ClockPosition, 0, 0); + POS.x += 28 * HUD.ResolutionScale; // XXX magic constant = bad + + C.SetOrigin(0.0, C.ClipY / 6); + OldClipX = C.ClipX; + C.ClipX = GetLongestEventListEntry(C, true) + 2 * POS.x; + + C.SetPos(0.0, 0.0); + C.SetDrawColor(0, 0, 0, 150); + C.DrawRect(C.ClipX, YL * Events.Length); + + for (Index=0; Index What) { function InterestingPickupTaken(PickupFactory F, class What, PlayerReplicationInfo Who) { local string Desc; + local SpectatorUI_EventParm parm; - if (Settings.PickupNotificationPattern != "") { - Desc = Settings.PickupNotificationPattern; - } else { - Desc = "`o has been picked up by `s."; - if (!bFollowPowerup) { - Desc = Desc @ Repl("Press `k to jump to that player.", "`k", GetKeyName(Settings.SwitchViewToButton)); - } - } - + Desc = Settings.PickupNotificationPattern != "" ? Settings.PickupNotificationPattern : "`o has been picked up by `s."; Desc = Repl(Desc, "`o", GetPickupName(What)); Desc = Repl(Desc, "`s", Who.GetPlayerAlias()); if (ShouldFollowPickup(What)) { RI.ViewPointOfInterest(); + } else { + parm.PickupActors = What; + parm.Message = Desc; + AddEventToCache(F, Who, parm); + + Desc = Desc @ Repl("Press `k to jump to that player.", "`k", GetSwitchToViewButtonString()); } PrintNotification(Desc); @@ -938,6 +1223,7 @@ function InterestingPickupTaken(PickupFactory F, class What, PlayerReplic function FlagEvent(UTCarriedObject Flag, name EventType, PlayerReplicationInfo Who) { local string Verb, Desc, Object; local byte Team; + local SpectatorUI_EventParm parm; Team = Flag.GetTeamNum(); @@ -982,7 +1268,10 @@ function FlagEvent(UTCarriedObject Flag, name EventType, PlayerReplicationInfo W } Desc = Desc $ "."; if (!bFollowPowerup) { - Desc = Desc @ Repl("Press `k to jump to the objective.", "`k", GetKeyName(Settings.SwitchViewToButton)); + parm.FlagEventType = EventType; + parm.Message = Desc; + AddEventToCache(Flag, Who, parm); + Desc = Desc @ Repl("Press `k to jump to the objective.", "`k", GetSwitchToViewButtonString()); } if (bFollowPowerup) { @@ -1038,6 +1327,58 @@ function bool IsKeyDoubleclicked(name Key, EInputEvent EventType) return bIsDoubleclick; } +function bool IsSwitchKeyOverridden() +{ + local name k, n; + k = Settings.SelectSwitchViewToButton; + n = Settings.SwitchViewToButton; + return (k != '' && (k == n || n == '')); +} + +function string GetSwitchToViewButtonString(optional bool bAlternate) +{ + local name Key; + local string s; + // XXX move OnShortManualActivated to instanced context to switch this to instanced context + Key = Settings.SelectSwitchViewToButton; + if (Key == '') Key = Settings.SwitchViewToButton; + s = GetKeyName(Key); + if (bAlternate && !IsSwitchKeyOverridden()) s = GetKeyName('LeftAlt') $" + "$s; + return s; +} + +function AddEventToCache(Actor EventActor, PlayerReplicationInfo Who, SpectatorUI_EventParm parm) { + local SpectatorUI_CachedEvent evt; + + // also remove entry with same event actor as this new one + ClearExpiredEvents(EventActor); + + evt.EventActor = EventActor; + evt.Instigator = Who; + evt.EventParameters = parm; + evt.Time = WorldInfo.GRI.TimeLimit != 0 ? WorldInfo.GRI.RemainingTime : WorldInfo.GRI.ElapsedTime; + evt.Expiring = WorldInfo.RealTimeSeconds + Settings.EventTimeout; + CachedEvents.AddItem(evt); + + // fallback + LastEventActor = EventActor; + LastEventPRI = Who; + if (SelectionInProgress != SS_None && SelectionMode == SELECTIONMODE_EVENTS) { + Events.AddItem(evt); + } +} + +function ClearExpiredEvents(optional Actor EventActorToRemove) { + local int i; + for (i=CachedEvents.Length-1; i>=0; i--) { + if (CachedEvents[i].Expiring < WorldInfo.RealTimeSeconds + || CachedEvents[i].EventActor == none + || CachedEvents[i].EventActor == EventActorToRemove) { + CachedEvents.Remove(i, 1); + } + } +} + defaultproperties { OnReceivedNativeInputKey=HandleInputKey diff --git a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ReplicationInfo.uc b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ReplicationInfo.uc index 5fd83ad..977acb9 100644 --- a/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ReplicationInfo.uc +++ b/UTGame/Src/SpectatorUI/Classes/SpectatorUI_ReplicationInfo.uc @@ -294,7 +294,7 @@ reliable server protected function ServerViewPointOfInterest() { if (Owner.IsInState('BaseSpectating')) { A = GetNextInterestingActor(); if (A == None) return; - PlayerController(Owner).SetViewTarget(A); + SwitchViewToPointOfInterest(A); } } @@ -302,8 +302,16 @@ simulated protected function DemoViewPointOfInterest() { local Actor A; A = GetNextInterestingActor(); if (A == None) return; + SwitchViewToPointOfInterest(A); +} + +simulated function SwitchViewToPointOfInterest(Actor A, optional PlayerReplicationInfo PRI) { + local Actor NewA; + if (PRI != none) NewA = PRI; + else if (UTCarriedObject(A) != none) NewA = UTCarriedObject(A).HolderPRI; + if (NewA != none) A = NewA; if (PlayerReplicationInfo(A) != None) { - DemoViewPlayer(PlayerReplicationInfo(A)); + ViewPlayer(PlayerReplicationInfo(A)); } else { PlayerController(Owner).SetViewTarget(A); }