swwmgz_m/zscript/handler/swwm_handler_worldtick.zsc
Marisa the Magician e5e6ce619c Fix severe performance issues in large maps.
So... Remember that one decision I made about avoiding BlockThingsIterator as
much as possible? Turns out that was a stupid idea. There ARE situations where
it's better to iterate sector thinglists, yes, especially for things that are
NOT part of the blockmap, but in other cases, the excess allocations of new
iterators are a reasonable price to pay for the lower perf impact in extreme
cases, such as maps that have a gazillion sectors with gazillions of things in
them (I'm looking at you, UDMF mappers).
As a compromise, at least, in situations where the thinglists are needed, I did
add a sort of micro-optimization by implementing code to check if a bounding
box is inside a sector (would be nice if this was part of GZDoom itself, tho).
2023-12-19 11:46:29 +01:00

355 lines
11 KiB
Text

// WorldTick functions
extend Class SWWMHandler
{
transient Array<Actor> combatactors;
transient Array<Int> combattics;
transient int highesttic, lastcombat;
int lastitemcount[MAXPLAYERS];
transient String curlang;
transient bool curfuntags;
SWWMSimpleTracker strackers;
bool mnotify;
bool allkills, allitems, allsecrets;
bool mapclear;
int mapclearagain, restartmus, startmus;
double musvol;
String lastmus;
int lastorder;
bool lastloop;
transient ThinkerIterator cti, qti;
private void LangRefresh()
{
if ( (language != curlang) || (swwm_funtags != curfuntags) )
{
// manually refresh some tags if language has changed
if ( !qti ) qti = ThinkerIterator.Create("SWWMQuickCombatTracker",Thinker.STAT_INVENTORY);
else qti.Reinit();
SWWMQuickCombatTracker qt;
while ( qt=SWWMQuickCombatTracker(qti.Next()) )
qt.UpdateTag(self);
for ( SWWMInterest p=intpoints; p; p=p.next )
{
if ( (p.type != INT_Key) || !p.trackedkey ) continue;
p.keytag = p.trackedkey.GetTag();
}
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( !playeringame[i] || !Demolitionist(players[i].mo) ) continue;
for ( SWWMItemSense s=Demolitionist(players[i].mo).itemsense; s; s=s.next )
s.UpdateTag();
}
}
curlang = language;
curfuntags = swwm_funtags;
}
// countable item scoring
private void ItemCountTrack()
{
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( !playeringame[i] ) continue;
if ( players[i].itemcount > lastitemcount[i] )
{
int score = 10*(players[i].itemcount-lastitemcount[i]);
if ( !deathmatch && !(gameinfo.gametype&GAME_Hexen) && (level.total_items == level.found_items) && !allitems )
{
allitems = true;
if ( i == consoleplayer ) Console.Printf(StringTable.Localize("$SWWM_LASTITEM"),500);
else Console.Printf(StringTable.Localize("$SWWM_LASTITEMREM"),players[i].GetUserName(),500);
score += 490;
SWWMUtility.AchievementProgressInc("allitems",1,players[i]);
}
SWWMCredits.Give(players[i],score);
SWWMScoreObj.SpawnFromHandler(self,score,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
lastitemcount[i] = players[i].itemcount;
let s = SWWMStats.Find(players[i]);
s.items++;
}
}
}
// combat tracking
private void CombatTrack()
{
// if players are above 1HP, reset the "one hp kill" counter
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( !playeringame[i] ) continue;
if ( players[i].Health != 1 ) onehpspree[i] = 0;
}
// prune old entries
for ( int i=0; i<combatactors.Size(); i++ )
{
if ( combattics[i] > highesttic )
highesttic = combattics[i];
if ( combatactors[i]
&& (combatactors[i].Health > 0)
&& !combatactors[i].bKILLED
&& !combatactors[i].bCORPSE
&& (combatactors[i].target == players[consoleplayer].mo)
&& (combattics[i]+2000 > gametic) )
continue;
combatactors.Delete(i);
combattics.Delete(i);
i--;
}
bool enteredcombat = false;
// add new entries
bool bossfound = false;
// we can use this instead of a thinker iterator as only actors that EXIST physically could count as combatants
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
{
if ( !a.player && !a.bISMONSTER ) continue;
// ignore the dead
if ( (a.Health <= 0) || a.bKILLED || a.bCORPSE ) continue;
// ignore if not targetted
if ( a.target != players[consoleplayer].mo ) continue;
// ignore friends
if ( a.IsFriend(players[consoleplayer].mo) ) continue;
// ignore if player can't see it
if ( !SWWMUtility.InPlayerFOV(players[consoleplayer],a) ) continue;
// is it already in?
bool addme = true;
for ( int i=0; i<combatactors.Size(); i++ )
{
if ( combatactors[i] != a ) continue;
addme = false;
combattics[i] = gametic;
break;
}
// add it in
if ( addme )
{
combatactors.Push(a);
combattics.Push(gametic);
enteredcombat = true;
if ( a.bBOSS || a.FindInventory("BossMarker") )
bossfound = true;
}
}
// be smart, demo-chan, don't shout if you're invisible, or you'll make it worse
if ( enteredcombat && ((bossfound && (!lastcombat || (gametic > lastcombat+240))) || (!bossfound && (!highesttic || (gametic > highesttic+700)))) && !players[consoleplayer].mo.FindInventory("GhostPower") )
lastcombat = AddOneliner("fightstart",1,10);
}
private void OneHundredPercentCheck()
{
// not in DM
if ( deathmatch ) return;
// not in Hexen, due to its fully hub-based nature
if ( gameinfo.gametype&GAME_Hexen ) return;
if ( !mapclear && (restartmus > 0) )
{
restartmus--;
if ( restartmus == 0 ) S_ChangeMusic(lastmus,lastorder,lastloop,true);
return;
}
// ignore levels that have NOTHING
if ( (level.total_secrets <= 0) && (level.total_items <= 0) && (level.total_monsters <= 0) ) return;
if ( mapclear )
{
if ( (musplaying.name != "music/nomusic.ogg") && (musplaying.name != "music/solitary.ogg") )
{
lastmus = musplaying.name;
lastorder = musplaying.baseorder;
lastloop = musplaying.loop;
S_ChangeMusic((startmus>0)?"music/nomusic.ogg":"music/solitary.ogg",force:true);
}
if ( startmus > 0 ) startmus--;
else if ( startmus == 0 )
{
startmus = -1;
S_ChangeMusic("music/solitary.ogg",force:true);
}
if ( (level.found_secrets < level.total_secrets) || (level.found_items < level.total_items) || (level.killed_monsters < level.total_monsters) )
{
restartmus = 25;
S_ChangeMusic("music/nomusic.ogg",force:true);
S_StartSound("recordscratch",CHAN_VOICE,CHANF_UI|CHANF_NOPAUSE|CHANF_OVERLAP,1,ATTN_NONE);
mapclear = false;
if ( mapclearagain > 1 ) Console.Printf(StringTable.Localize("$SWWM_NOTCLEARAGAIN"));
else Console.Printf(StringTable.Localize("$SWWM_NOTCLEAR"));
}
return;
}
if ( (level.found_secrets < level.total_secrets) || (level.found_items < level.total_items) || (level.killed_monsters < level.total_monsters) ) return;
restartmus = 0;
mapclear = true;
if ( mapclearagain ) Console.Printf(StringTable.Localize("$SWWM_ALLCLEARAGAIN"),500);
else Console.Printf(StringTable.Localize("$SWWM_ALLCLEAR"),5000);
bool altclear = swwm_altclear;
S_StartSound(altclear?"misc/yippeee":"misc/wow",CHAN_VOICE,CHANF_UI|CHANF_NOPAUSE|CHANF_OVERLAP,1,ATTN_NONE);
lastmus = musplaying.name;
lastorder = musplaying.baseorder;
lastloop = musplaying.loop;
S_ChangeMusic("music/nomusic.ogg",force:true);
startmus = 1050;
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( !playeringame[i] || !players[i].mo ) continue;
let f = Actor.Spawn("PartyTime",players[i].mo.pos);
if ( altclear ) f.bSTANDSTILL = true;
else f.bAMBUSH = true;
if ( mapclearagain )
{
SWWMCredits.Give(players[i],500);
SWWMScoreObj.SpawnFromHandler(self,500,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
}
else
{
SWWMCredits.Give(players[i],5000);
SWWMScoreObj.SpawnFromHandler(self,5000,players[i].mo.Vec3Offset(0,0,players[i].mo.Height/2));
}
}
mapclearagain++;
if ( !iwantdie ) return;
for ( int i=0; i<MAXPLAYERS; i++ )
{
if ( !playeringame[i] ) continue;
let demo = Demolitionist(players[i].mo);
if ( !demo || !demo.mystats ) continue;
if ( demo.mystats.deaths > 0 ) return;
}
SWWMUtility.MarkAchievement("wantdie",players[consoleplayer]);
}
private void UpdateHUDObjects()
{
// score objects
SWWMScoreObj so = scorenums;
SWWMScoreObj soprev = null, sonext;
while ( so )
{
sonext = so.next;
if ( so.Tick() )
{
if ( soprev ) soprev.next = sonext;
else scorenums = sonext;
so.Destroy();
}
else soprev = so;
so = sonext;
}
SWWMDamNum dn = damnums;
SWWMDamNum dnprev = null, dnnext;
while ( dn )
{
dnnext = dn.next;
if ( dn.Tick() )
{
if ( dnprev ) dnprev.next = dnnext;
else damnums = dnnext;
dn.Destroy();
}
else dnprev = dn;
dn = dnnext;
}
// interest markers
SWWMInterest ip = intpoints;
SWWMInterest ipprev = null, ipnext;
while ( ip )
{
ipnext = ip.next;
if ( ip.Tick() )
{
if ( ipprev ) ipprev.next = ipnext;
else intpoints = ipnext;
ip.Destroy();
}
else ipprev = ip;
ip = ipnext;
}
}
// "simple" tracking (used by the minimap)
private void SimpleTracking()
{
if ( (gamestate != GS_LEVEL) || !swwm_mm_enable )
{
while ( strackers )
{
SWWMSimpleTracker next = strackers.next;
strackers.Destroy();
strackers = next;
}
return;
}
// update trackers for anything around the player
double viewdist = SWWMStatusBar.MAPVIEWDIST;
// still about as expensive as using a BlockThingsIterator, but without the need to allocate one every tic
int thisgroup = players[consoleplayer].Camera.CurSector.portalgroup;
foreach ( s:level.Sectors )
{
// don't check sectors that aren't within bounds, saves some time
if ( !BoxInSectorBounds(s,players[consoleplayer].Camera.pos.xy,viewdist,players[consoleplayer].Camera.CurSector.PortalGroup) )
continue;
for ( Actor a=s.thinglist; a; a=a.snext )
{
Vector2 rv;
if ( s.portalgroup != thisgroup )
{
Vector2 relpos = players[consoleplayer].Camera.pos.xy+level.GetDisplacement(thisgroup,s.portalgroup);
rv = a.pos.xy-relpos;
}
else rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
double rad;
bool isproj = a.bMISSILE&&SWWMUtility.ValidProjectile(a);
if ( SWWMUtility.IsBeamProj(a) )
{
isproj = true;
rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
}
else rad = a.radius;
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
continue;
if ( a == players[consoleplayer].Camera )
continue;
if ( a is 'GhostTarget' )
continue;
if ( !a.player && !a.bSOLID && !a.bSHOOTABLE && !a.bISMONSTER && !a.bFRIENDLY && !(a is 'Inventory') && !(a is 'Chancebox') && !isproj )
continue;
if ( !level.allmap && !(deathmatch && (a is 'Inventory') && !a.bDROPPED) && !(a.IsFriend(players[consoleplayer].mo) && !(a.player && (a.player.mo != a))) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
if ( a.bKILLED || (a.Health <= 0) || a.bUnmorphed )
continue;
if ( (a is 'Inventory') && (!a.bSPECIAL || Inventory(a).Owner || (a.GetClassName() == 'aas_token')) ) // autoautosave hotfix
continue;
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
continue;
if ( isproj && !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
SWWMSimpleTracker.Track(self,a);
}
}
SWWMSimpleTracker trk = strackers;
SWWMSimpleTracker prev = null, next;
while ( trk )
{
next = trk.next;
// minimize lifespan of destroyed targets
if ( !trk.target ) trk.lastupdate = min(trk.lastupdate,level.maptime);
else if ( !trk.expired )
{
// "last breath" update
if ( (trk.target.bKILLED || (trk.target.Health <= 0))
|| ((trk.target is 'Inventory') && (!trk.target.bSPECIAL || Inventory(trk.target).Owner))
|| ((trk.target is 'Chancebox') && (trk.target.CurState != trk.target.SpawnState))
|| (trk.target.default.bMISSILE && !trk.target.bMISSILE)
|| trk.target.bUnmorphed )
trk.Update();
}
// prune expired trackers
if ( trk.lastupdate+140 < level.maptime )
{
if ( !prev ) strackers = trk.next;
else prev.next = trk.next;
trk.Destroy();
}
else prev = trk;
trk = next;
}
}
}