· 5 years ago · Mar 07, 2020, 06:13 PM
1local PLAYER = FindMetaTable( "Player" )
2local Timer, Zones, Player, RTV = { Spawns = {}, Teleports = {}, Top = {}, PostInitFunc = {} }, { SpecialDoorMaps = {}, MovingDoorMaps = {} }, {}, {}
3local StylePoints, TopListCache, WRCache, WRTopCache, Checkpoints, BeatMaps, WRSounds, Popups = {}, {}, {}, {}, {}, {}, {}, {}
4local MR, MA, MC, FL, FO, SU, OD, OT, ST, HAS = math.random, math.abs, math.ceil, math.floor, string.format, string.sub, os.date, os.time, SysTime, table.HasValue
5local Config, NetPrepare = Core.Config, Core.Prepare
6local Styles, Ranks, PlayerData = Config.Style, Config.Ranks, Config.Player
7local ScrollStyles = { [Styles["Easy Scroll"]] = true, [Styles["Legit"]] = true }
8local BoostTimer, BoostCooldown, BoostMultiplier = {}, { 30, 45, 20, 20 }, { 1.8, 2.4, 3.0, 3.0 }
9
10--[[
11 Description: Translates a zone box into usable coordinates and gets a random spawn point
12 Used by: Respawning functionality
13 Notes: Looks messy, but it works
14--]]
15local function GetSpawnPoint( data )
16 if type( data ) != "table" or #data != 3 then
17 return Vector( 0, 0, 0 )
18 end
19
20 local vx, vy, vz = 8, 8, 0
21 local dx, dy, dz = data[ 2 ].x - data[ 1 ].x, data[ 2 ].y - data[ 1 ].y, data[ 2 ].z - data[ 1 ].z
22
23 if dx > 96 then vx = dx - 32 - ((data[ 2 ].x - data[ 1 ].x) / 2) end
24 if dy > 96 then vy = dy - 32 - ((data[ 2 ].y - data[ 1 ].y) / 2) end
25 if dz > 32 then vz = 16 end
26
27 local center = Vector( data[ 3 ].x, data[ 3 ].y, data[ 1 ].z )
28 local out = center + Vector( MR( -vx, vx ), MR( -vy, vy ), vz )
29
30 return out
31end
32Core.RandomizeSpawn = GetSpawnPoint
33
34--[[
35 Description: Checks if the player has a valid timer
36 Used by: Timing functions (validation)
37 Notes: Created a function for this to keep the timing functions cleaner
38--]]
39local function ValidTimer( ply, bBonus )
40 if not IsValid( ply ) or not ply.Style then return false end
41 if ply.Practice or ply.TAS then return false end
42 if ply.SkipValidation then ply.SkipValidation = nil return false end
43
44 if bBonus then
45 if not Core.IsValidBonus( ply.Style ) then return false end
46 else
47 if Core.IsValidBonus( ply.Style ) then return false end
48 end
49
50 return true
51end
52
53--[[
54 Description: Resets any game-changing attributes on the player
55 Used by: Respawning and timer starting
56--]]
57local function ResetPlayerAttributes( ply, nPrevious, bStart )
58 if ply:GetMoveType() != 2 then
59 ply:SetMoveType( 2 )
60 end
61
62 if ply.LastObtainedFinish then
63 ply.LastObtainedFinish = nil
64 end
65
66 if ply.TAS or ply.Practice then
67 ply:SetStrafeStats()
68 end
69
70 if ply.TnId and not bStart then
71 ply.TnId = nil
72 end
73
74 if nPrevious then
75 ply:StageReset()
76
77 if nPrevious == Styles.Unreal then
78 BoostTimer[ ply ] = nil
79
80 local ar = NetPrepare( "Timer/UnrealReset" )
81 ar:UInt( 0, 6 )
82 ar:Send( ply )
83 end
84
85 if ply.Style == Styles.Legit or ply.Style == Styles.Stamina then
86 ply:EnableStamina( true )
87 elseif ply.StaminaUse then
88 ply:EnableStamina( false )
89 end
90 end
91
92 if ply.Style == Styles["Low Gravity"] then
93 ply:SetGravity( Config.Player.LowGravity )
94 elseif ply.Style == Styles["M.L.G"] then
95 ply:SetGravity( 0.25 )
96 elseif ply.Style == Styles["High Gravity"] then
97 ply:SetGravity( 1.4 )
98 elseif ply.Style == Styles["Moon Man"] or ply.Style == Styles["Cancer"] then
99 ply:SetGravity( 0.1 )
100 elseif ply:GetGravity() != 0 then
101 ply:SetGravity( 0 )
102 end
103
104 if ScrollStyles[ ply.Style ] and ply.RequestJumpRatio then
105 ply:RequestJumpRatio( true )
106 end
107
108 if ply.Practice then return end
109 if ply:GetLaggedMovementValue() != 1 then
110 ply:SetLaggedMovementValue( 1 )
111 end
112end
113
114--[[
115 Description: Cleaning up of variables when the player resets or sets their timer
116 Used by: Timing functionality
117 Notes: It's quite a small function but could be expanded easily
118--]]
119local function PostTimerCleanup( ply, szType, varData )
120 ply:Spectator( "PlayerRestart", varData )
121 ply:SetStrafeStats()
122
123 if szType == "Start" or szType == "Reset" then
124 ResetPlayerAttributes( ply, nil, szType == "Start" )
125 end
126end
127
128
129--[[
130 Description: Attempts to start the player's timer
131 Used by: Start zone entity
132--]]
133function PLAYER:StartTimer( ent )
134 if self.TAS then return self.TAS.StartTimer( self, ent ) end
135 if not ValidTimer( self ) then return end
136
137 local vel2d = self:GetVelocity():Length2D()
138 if self.Style != Core.Config.Style["Prespeed"] and vel2d > PlayerData.StartSpeed and not Zones.IsOption( Zones.Options.NoStartLimit ) then
139 self:ResetSpawnPosition()
140 return Player.Notification( self, "Popup", { "Timer", Core.Text( "ZoneSpeed", MC( vel2d ) .. " u/s" ), "lightning", 4 } )
141 end /*elseif Config.IsBhop and self.Style != Core.Config.Style["Prespeed"] and vel2d > 0 and MA( self:GetPos().z - ent.min.z ) > 5 and ent.max.z - ent.min.z > 8 then
142 self:ResetSpawnPosition()
143 return Player.Notification( self, "Popup", { "Timer", Core.Text( "ZoneJumpExit" ), "error", 4 } )
144 end*/
145
146 -- Set the start speed value for surfers
147 if Config.IsSurf then
148 self.Tspeed = self:GetVelocity():Length()
149 end
150
151 self.Tn = ST()
152 self:SetJumps( 0 )
153
154 local ar = NetPrepare( "Timer/Start" )
155 ar:UInt( 1, 2 )
156 ar:Send( self )
157
158 -- Make sure we don't have the FULL clip of this guy walking around in the start zone
159 self:ChopFrames( true )
160 self.InSpawn = nil
161
162 -- Do some stage checking
163 if self.TnB then
164 self.TnB = self:GetCurrentFrame()
165 end
166
167 -- Check multiple start points
168 if #Zones.StartPoints > 1 and IsValid( ent ) then
169 local id
170 for pos,data in pairs( Zones.StartPoints ) do
171 if data[ 1 ] == ent.min then
172 id = pos
173 end
174 end
175
176 self.TzI = id
177 end
178
179 PostTimerCleanup( self, "Start" )
180end
181
182--[[
183 Description: Resets a player's timer
184 Used by: Start zone entity, spawning functions
185--]]
186function PLAYER:ResetTimer( bEntity, ent )
187 if self.TAS then return self.TAS.ResetTimer( self, ent ) end
188 if not ValidTimer( self ) then return end
189 if not self.Tn and not bEntity then return end
190
191 self.Tn = nil
192 self.TnF = nil
193
194 self:CleanFrames()
195 self:SetBotActive( true )
196
197 if bEntity then
198 self.InSpawn = true
199 end
200
201 PostTimerCleanup( self, "Reset" )
202
203 local ar = NetPrepare( "Timer/Start" )
204 ar:UInt( 0, 2 )
205 ar:Send( self )
206end
207
208--[[
209 Description: Stops the timer and ends the run
210 Used by: End zone entity
211--]]
212function PLAYER:StopTimer( ent )
213 if self.TAS then return self.TAS.StopTimer( self, ent ) end
214 if not ValidTimer( self ) then return end
215 if not self.Tn or self.TnF then return end
216
217 self.TnF = ST()
218 self:SetBotActive( nil )
219
220 -- Start making use of this time that the player got
221 local nTime = self.TnF - self.Tn
222 Timer.ProcessEnd( self, nTime )
223 PostTimerCleanup( self, "Stop", { nTime } )
224end
225
226--[[
227 Description: Attempts to start the player's bonus timer
228 Used by: Bonus start zone entity
229--]]
230function PLAYER:BonusStart( ent )
231 if IsValid( ent ) and not Zones.ValidateBonusStyle( self, ent.embedded ) then return end
232 if self.TAS then return self.TAS.StartTimer( self, ent, true ) end
233 if not ValidTimer( self, true ) then return end
234
235 local vel2d = self:GetVelocity():Length2D()
236 if vel2d > PlayerData.StartSpeed then
237 self:ResetSpawnPosition()
238 return Player.Notification( self, "Popup", { "Timer", Core.Text( "ZoneSpeed", MC( vel2d ) .. " u/s" ), "lightning", 4 } )
239 end
240 /*elseif Config.IsBhop and vel2d > 0 and MA( self:GetPos().z - ent.min.z ) > 5 and ent.max.z - ent.min.z > 8 then
241 self:ResetSpawnPosition()
242 return Player.Notification( self, "Popup", { "Timer", Core.Text( "ZoneJumpExit" ), "error", 4 } )
243 end*/
244
245 -- Set the start speed value for surfers
246 if Config.IsSurf then
247 self.Tspeed = self:GetVelocity():Length()
248 end
249
250 self.Tb = ST()
251 self:SetJumps( 0 )
252
253 local ar = NetPrepare( "Timer/Start" )
254 ar:UInt( 1, 2 )
255 ar:Send( self )
256
257 -- Make sure we don't have the FULL clip of this guy walking around in the start zone
258 self:ChopFrames( true )
259 self.InSpawn = nil
260
261 PostTimerCleanup( self, "Start" )
262end
263
264--[[
265 Description: Resets a player's bonus timer
266 Used by: Bonus start zone entity, spawning functions
267--]]
268function PLAYER:BonusReset( bEntity, ent )
269 if bEntity and IsValid( ent ) and not Zones.ValidateBonusStyle( self, ent.embedded ) then return end
270 if self.TAS then return self.TAS.ResetTimer( self, ent, true ) end
271 if not ValidTimer( self, true ) then return end
272 if not self.Tb and not bEntity then return end
273
274 self.Tb = nil
275 self.TbF = nil
276
277 self:CleanFrames()
278 self:SetBotActive( true )
279
280 if bEntity then
281 self.InSpawn = true
282 end
283
284 PostTimerCleanup( self, "Reset" )
285
286 local ar = NetPrepare( "Timer/Start" )
287 ar:UInt( 0, 2 )
288 ar:Send( self )
289end
290
291--[[
292 Description: Stops the bonus timer and ends the run
293 Used by: Bonus end zone entity
294--]]
295function PLAYER:BonusStop( ent )
296 if IsValid( ent ) and not Zones.ValidateBonusStyle( self, ent.embedded ) then return end
297 if self.TAS then return self.TAS.StopTimer( self, ent, true ) end
298 if not ValidTimer( self, true ) then return end
299 if not self.Tb or self.TbF then return end
300
301 self.TbF = ST()
302 self:SetBotActive( nil )
303
304 -- Start making use of this sexy bonus time
305 local nTime = self.TbF - self.Tb
306 Timer.ProcessEnd( self, nTime )
307 PostTimerCleanup( self, "Stop", { nTime } )
308end
309
310--[[
311 Description: Stops any timer (for cheating purposes)
312 Used by: Anti-cheat zones, +left and +right checker
313--]]
314function PLAYER:StopAnyTimer( ent )
315 if self:IsBot() or self.Practice then return false end
316 if IsValid( ent ) and ent.embedded and self.Style != ent.embedded then return false end
317 if self.TAS then return self.TAS.ResetTimer( self, ent ) end
318
319 self.Tn = nil
320 self.TnF = nil
321 self.Tb = nil
322 self.TbF = nil
323
324 self:StageReset()
325 self:SetBotActive( nil )
326 self:CleanFrames()
327
328 local ar = NetPrepare( "Timer/Start" )
329 ar:UInt( 0, 2 )
330 ar:Send( self )
331
332 PostTimerCleanup( self, "Anticheat" )
333
334 return true
335end
336
337--[[
338 Description: Resets the player's position
339 Used by: Spawning functions, zone limitations
340--]]
341function PLAYER:ResetSpawnPosition( tabCheck, bReset, bLeave )
342 if self:IsBot() then
343 self:SetLocalVelocity( Vector( 0, 0, 0 ) )
344 return Zones.BotPoints and #Zones.BotPoints > 0 and self:SetPos( Zones.BotPoints[ RTV.TrueRandom( 1, #Zones.BotPoints ) ] )
345 elseif tabCheck then
346 if Core.IsValidBonus( tabCheck[ 1 ] ) then
347 return not Zones.GetBonusPoint( tabCheck[ 2 ] )
348 end
349
350 return false
351 end
352
353 if not self.Style then return end
354
355 if bReset then
356 local dz = bReset.embedded
357 if dz then
358 if bLeave and self:GetVelocity():Length2D() > dz * 100 then
359 self:SetLocalVelocity( Vector( 0, 0, 0 ) )
360 end
361
362 return
363 end
364 end
365
366 self.LastResetData = not self.InSpawn and { ST(), self.Style, self.TnF, self.Tn, self:GetPos(), self:EyeAngles(), self:GetJumps(), self:GetStrafes(), { self:GetSync( true ) } }
367 self:SetLocalVelocity( Vector( 0, 0, 0 ) )
368 self:SetJumps( 0 )
369 self:SetJumpPower( Config.Player.JumpPower )
370 self:StageReset()
371
372 ResetPlayerAttributes( self )
373
374 if self.IsLJ then
375 self:LJResetStats()
376 end
377
378 if self.SpaceEnabled then
379 Core.Send( self, "Timer/Space", true )
380 end
381
382 if self.Style == Styles.Unreal then
383 BoostTimer[ self ] = nil
384
385 local ar = NetPrepare( "Timer/UnrealReset" )
386 ar:UInt( 0, 6 )
387 ar:Send( self )
388 end
389
390 local bonus = Core.IsValidBonus( self.Style )
391 if bonus and self.Tb then
392 self:BonusReset()
393 elseif not bonus and self.Tn then
394 self:ResetTimer()
395 end
396
397 if Timer.BaseAngles then
398 if bonus then
399 local ang = Timer.BonusAngles[ self.Style - Styles.Bonus ]
400 if ang then
401 self:SetEyeAngles( Angle( self:EyeAngles().p, ang.y, 0 ) )
402 end
403 else
404 self:SetEyeAngles( Angle( self:EyeAngles().p, Timer.BaseAngles.y, 0 ) )
405 end
406 end
407
408 if not bonus and Zones.StartPoints and #Zones.StartPoints > 0 then
409 self:SetPos( GetSpawnPoint( Zones.StartPoints[ self.TzI or RTV.TrueRandom( 1, #Zones.StartPoints ) ] ) )
410 self.InSpawn = true
411 elseif bonus then
412 self:SetPos( GetSpawnPoint( Zones.GetBonusPoint( self.Style - Styles.Bonus ) ) )
413 else
414 Core.Print( self, "Timer", Core.Text( "ZoneSetup" ) )
415 end
416
417 return true
418end
419
420--[[
421 Description: Executes an unreal boost
422 Used by: Movement and KeyPress hook
423--]]
424function PLAYER:DoUnrealBoost( nForce, nMultiplierOverride, nForceCooldown, vForceVelocity )
425 if BoostTimer[ self ] and ST() < BoostTimer[ self ] then return end
426 if self.TAS and self.TAS.UnrealBoost( self ) then return end
427
428 -- Set the base cooldown to be non-existant
429 local nCooldown, nMultiplier, nType = 0, 0, 1
430 local vel = self:GetVelocity()
431
432 -- Check which boost type we need
433 if self:KeyDown( IN_FORWARD ) and not self:KeyDown( IN_BACK ) and not self:KeyDown( IN_MOVELEFT ) and not self:KeyDown( IN_MOVERIGHT ) then
434 nType = 2
435 elseif self:KeyDown( IN_JUMP ) and not self:KeyDown( IN_FORWARD ) and not self:KeyDown( IN_BACK ) and not self:KeyDown( IN_MOVELEFT ) and not self:KeyDown( IN_MOVERIGHT ) then
436 nType = 3
437 elseif self:KeyDown( IN_BACK ) and not self:KeyDown( IN_FORWARD ) and not self:KeyDown( IN_MOVELEFT ) and not self:KeyDown( IN_MOVERIGHT ) then
438 nType = 4
439 else
440 nType = 1
441 end
442
443 -- See if we're forcing
444 if nForce then
445 nType = nForce
446 end
447
448 -- By default, for all different key combinations, we will simply amplify velocity
449 if vForceVelocity then
450 self:SetVelocity(vForceVelocity)
451 elseif nType == 1 then
452 nCooldown = BoostCooldown[ 1 ]
453 if nMultiplierOverride then
454 nMultiplier = nMultiplierOverride
455 else
456 nMultiplier = BoostMultiplier[ 1 ]
457 end
458
459 self:SetVelocity( vel * Vector( nMultiplier, nMultiplier, nMultiplier * 1.5 ) - vel )
460
461 -- If we've only got W down, we will boost forward faster than normal omnidirectional boost
462 elseif nType == 2 then
463 nCooldown = BoostCooldown[ 2 ]
464 if nMultiplierOverride then
465 nMultiplier = nMultiplierOverride
466 else
467 nMultiplier = BoostMultiplier[ 2 ]
468 end
469
470 self:SetVelocity( vel * Vector( nMultiplier, nMultiplier, 1 ) - vel )
471
472 -- If we've only got jump in, we will boost upwards strongly
473 elseif nType == 3 then
474 nCooldown = BoostCooldown[ 3 ]
475 if nMultiplierOverride then
476 nMultiplier = nMultiplierOverride
477 else
478 nMultiplier = BoostMultiplier[ 3 ]
479 end
480
481 if vel.z < 0 then
482 nMultiplier = -0.5 * nMultiplier
483 end
484
485 self:SetVelocity( vel * Vector( 1, 1, nMultiplier ) - vel )
486
487 -- If we've got S down and nothing else, we will boost downwards fast
488 elseif nType == 4 then
489 nCooldown = BoostCooldown[ 4 ]
490 if nMultiplierOverride then
491 nMultiplier = nMultiplierOverride
492 else
493 nMultiplier = BoostMultiplier[ 4 ]
494 end
495
496 if vel.z > 0 then
497 nMultiplier = -nMultiplier
498 end
499
500 self:SetVelocity( vel * Vector( 1, 1, nMultiplier ) - vel )
501 end
502
503 if nForceCooldown != nil then
504 nCooldown = nForceCooldown
505 end
506
507 if nCooldown != 0 then
508 BoostTimer[ self ] = ST() + nCooldown
509 if self.TAS then self.TAS.UnrealBoost( self, nCooldown ) end
510
511 local ar = NetPrepare( "Timer/UnrealReset" )
512 ar:UInt( nCooldown, 6 )
513 ar:Send( self )
514 end
515end
516
517--[[
518 Description: Enables stamina on the given player
519 Used by: Stamina styles
520--]]
521function PLAYER:EnableStamina( bool )
522 EnableStamina( self, bool )
523 self.StaminaUse = bool
524
525 local ar = NetPrepare( "Timer/Stamina" )
526 ar:Bit( bool )
527 ar:Send( self )
528
529 return bool
530end
531
532--[[
533 Description: Enables freestyle movement for specific styles
534 Used by: Freestyle zone entity (ENTER)
535--]]
536function PLAYER:StartFreestyle()
537 if not ValidTimer( self ) then return end
538
539 if self.Style >= Styles.SW and self.Style <= Styles["S-Only"] then
540 self.Freestyle = true
541 Core.Send( self, "Timer/Freestyle", { self.Freestyle } )
542 Core.Print( self, "Timer", Core.Text( "StyleFreestyle", "entered a", " All key combinations are now possible." ) )
543 elseif self.Style == Styles["Low Gravity"] then
544 self.Freestyle = true
545 Core.Send( self, "Timer/Freestyle", { self.Freestyle } )
546 Core.Print( self, "Timer", Core.Text( "StyleFreestyle", "entered a", " Reverted gravity to normal values." ) )
547 end
548end
549
550--[[
551 Description: Disables freestyle movement for specific styles
552 Used by: Freestyle zone entity (LEAVE)
553--]]
554function PLAYER:StopFreestyle()
555 if not ValidTimer( self ) then return end
556
557 if self.Style >= Styles.SW and self.Style <= Styles["S-Only"] then
558 self.Freestyle = nil
559 Core.Send( self, "Timer/Freestyle", { self.Freestyle } )
560 Core.Print( self, "Timer", Core.Text( "StyleFreestyle", "left the", "" ) )
561 elseif self.Style == Styles["Low Gravity"] then
562 self.Freestyle = nil
563 Core.Send( self, "Timer/Freestyle", { self.Freestyle } )
564 Core.Print( self, "Timer", Core.Text( "StyleFreestyle", "left the", "" ) )
565 end
566end
567
568
569-- Records
570
571Timer.Multiplier = 1
572Timer.BonusMultiplier = 1
573Timer.Options = 0
574Timer.Maps = 0
575
576local Maps = {}
577local Records = {}
578local TopTimes = {}
579local Averages = {}
580local TimeCache = {}
581local Prepare, InsertAt, RemoveAt = SQLPrepare, table.insert, table.remove
582
583
584--[[
585 Description: Gets the amount of records in a style
586--]]
587local function GetRecordCount( nStyle )
588 return Records[ nStyle ] and #Records[ nStyle ] or 0
589end
590
591--[[
592 Description: Gets the saves average for a specific style, returns 0 if none set
593--]]
594local function GetAverage( nStyle )
595 return Averages[ nStyle ] or 0
596end
597
598--[[
599 Description: Recalculate the average for a specific style
600--]]
601local function CalcAverage( nStyle )
602 local nTotal, nCount = 0, 0
603
604 -- Iterate over the top 50 times of this style and add the time values
605 for i = 1, 50 do
606 if Records[ nStyle ] and Records[ nStyle ][ i ] then
607 nTotal = nTotal + Records[ nStyle ][ i ]["nTime"]
608 nCount = nCount + 1
609 else
610 break
611 end
612 end
613
614 -- Check the amount of times we have
615 if nCount == 0 then
616 Averages[ nStyle ] = 0
617 else
618 -- Save the average for later use
619 Averages[ nStyle ] = nTotal / nCount
620 end
621
622 -- Return the saved average
623 return Averages[ nStyle ]
624end
625
626--[[
627 Description: Updates a player record accordingly
628--]]
629local function UpdateRecords( ply, nPos, nNew, nOld, nDate )
630 local Entry = {}
631 Entry.szUID = ply.UID
632 Entry.nTime = nNew
633 Entry.nPoints = 0 -- These will be inserted directly afterwards
634 Entry.nDate = nDate
635 Entry.vData = nil
636
637 -- Set the details position
638 ply.SpeedPos = nPos
639
640 -- If there's no previous time, just insert a new entry at the correct position
641 if nOld == 0 then
642 InsertAt( Records[ ply.Style ], nPos, Entry )
643 else
644 local AtID = 0
645
646 -- Obtain the player's location in the ladder
647 for i = 1, #Records[ ply.Style ] do
648 if Records[ ply.Style ][ i ]["szUID"] == Entry["szUID"] then
649 AtID = i
650 break
651 end
652 end
653
654 -- Update the record at that position
655 if AtID > 0 then
656 RemoveAt( Records[ ply.Style ], AtID )
657 InsertAt( Records[ ply.Style ], nPos, Entry )
658 else
659 print( "Records", "Unable to replace existing time. Please restart server immediately." )
660 end
661 end
662end
663
664--[[
665 Description: Final function in the AddRecord chain; broadcasts messages and recalculates
666--]]
667local function AddRecord_End( ply, nTime, nOld, nID, nStyle, nPreviousWR, nPrevID, szPreviousWR )
668 -- Setup data for notification
669 local data = {}
670
671 -- Give them a shiny medal when applicable
672 if nID <= 3 then
673 Player.SetRankMedal( ply, nID, true )
674
675 if nID == 1 then
676 -- Notify the previous WR holder
677 if nPreviousWR > 0 and szPreviousWR and szPreviousWR != ply.UID then
678 Player.NotifyBeatenWR( szPreviousWR, Timer:GetMap(), ply:Name(), nStyle, nPreviousWR - nTime )
679 end
680
681 -- (Re)load the full list if required
682 if nStyle == Styles.Normal or GetRecordCount( nStyle ) >= 10 then
683 if not Timer.SoundTracker or #Timer.SoundTracker == 0 then
684 Timer.SoundTracker = {}
685
686 for i = 1, #WRSounds do
687 Timer.SoundTracker[ i ] = i
688 end
689 end
690
691 -- WR Sounds, yey (only for the really cool people, though)
692 local nSound = table.remove( Timer.SoundTracker, RTV.TrueRandom( 1, #Timer.SoundTracker ) )
693 data.Sound = "/sound/" .. Config.MaterialID .. "/" .. WRSounds[ nSound ] .. ".mp3"
694 end
695
696 -- Set the top time
697 Timer.ChangeTopTime( nStyle )
698 end
699 end
700
701 -- Get the new WR position for the bot
702 Core.SetBotRecord( nStyle, nID )
703
704 -- Send the player his new time
705 local ar = NetPrepare( "Timer/Record" )
706 ar:Double( nTime )
707 ar:Bit( false )
708 ar:Bit( true )
709 ar:Send( ply )
710
711 -- End the bot run if there is any
712 local bSucceed = ply:EndBotRun( nTime, nID )
713 if bSucceed then
714 data.Bot = true
715 else
716 ply.LastObtainedFinish = { nTime, nStyle, Core.IsValidBonus( nStyle ) and ply.TbF or ply.TnF, ply.BotFrameStart }
717
718 -- Check if we are being force recorded
719 if ply.BotForce then
720 timer.Simple( 1, function()
721 if IsValid( ply ) then
722 if Core.ForceBotSave( ply ) then
723 if IsValid( ply.BotForce ) then
724 if ply.BotForce == ply then
725 Core.Print( ply, "General", Core.Text( "CommandBotForceSaved" ) )
726 else
727 Core.Print( ply.BotForce, "General", Core.Text( "CommandBotForceFeedback", ply:Name() ) )
728 end
729
730 ply.BotForce = nil
731 end
732 end
733 end
734 end )
735 end
736 end
737
738 -- Setup the variables
739 data.Time = nTime
740 data.Style = nStyle
741 data.Pos = nID
742 data.DifferenceWR = nID > 1 and "WR +" .. Timer.Convert( nTime - Timer.ChangeTopTime( nStyle, true ) ) or (nPreviousWR > 0 and "WR -" .. Timer.Convert( nPreviousWR - nTime ) or "")
743 data.Improvement = nOld == 0 and -1 or Timer.Convert( nOld - nTime )
744 data.MapRecord = nID == 1
745 data.Rank = nID .. " / " .. GetRecordCount( nStyle )
746
747 -- Send out the notification
748 Player.Notification( ply, "ImproveFinish", data )
749
750 -- Finally publish the changes made to the player
751 ply:PublishObj()
752end
753
754--[[
755 Description: First function in the AddRecord chain; Get the player's new rank
756 Used by: SQL callback
757--]]
758local function AddRecord_Begin( data, varArg, szError )
759 -- Get variables
760 local ply, nTime, nOld, nDate, nStyle = varArg[ 1 ], varArg[ 2 ], varArg[ 3 ], varArg[ 4 ], varArg[ 5 ]
761
762 -- Get the previous WR if there was any
763 local nPrevious, szPrevious = Timer.ChangeTopTime( nStyle, true )
764
765 -- Get the new position in the ladder
766 local nID = Timer.GetRecordID( nTime, nStyle )
767
768 -- Get the current position in the ladder
769 local _,nPrevID = Timer.GetPlayerRecord( ply )
770
771 -- Calculate the current average
772 local nCurrentAverage = CalcAverage( nStyle )
773
774 -- Insert the record into the internal table
775 UpdateRecords( ply, nID, nTime, nOld, nDate )
776
777 -- Obtain the new average
778 CalcAverage( nStyle )
779
780 -- Change the ID
781 ply.Leaderboard = nID
782 ply:SetObj( "Position", ply.Leaderboard )
783
784 -- Reload everything
785 ply:LoadRank() -- Reload the main player's rank
786 ply:AddFrags( 1 ) -- This shows up on GameTracker, it's cool
787 Player.ReloadRanks( ply, nStyle, nCurrentAverage ) -- Reload the ranks for other players
788
789 -- End the AddRecord instance
790 AddRecord_End( ply, nTime, nOld, nID, nStyle, nPrevious or 0, nPrevID, szPrevious )
791end
792
793
794--[[
795 Description: Begins processing the obtained time and takes the next steps
796 Used by: Timing functions
797--]]
798function Timer.ProcessEnd( ply, nTime )
799 -- Get the difference between previous record
800 local Difference = ply.Record > 0 and nTime - ply.Record
801 local IsImproved = ply.Record == 0 or (ply.Record > 0 and nTime < ply.Record)
802 local SelfDifference = Difference and "PB " .. (Difference < 0 and "-" or "+") .. Timer.Convert( MA( Difference ) ) or ""
803
804 -- Check run details
805 local CurrentSync, Strafes, Jumps = ply:GetSync(), ply:GetStrafes(), ply:GetJumps()
806 if CurrentSync then
807 -- Set their values for updating with speed
808 ply.LastSync = CurrentSync
809 ply.LastStrafes = Strafes
810 ply.LastJumps = Jumps
811
812 -- Get specific values
813 if ScrollStyles[ ply.Style ] then
814 ply.LastRatio = ply.RequestJumpRatio and ply:RequestJumpRatio( nil, { Core.StyleName( ply.Style ), Jumps, nTime, ply.Record } )
815 end
816 end
817
818 -- Set start speed
819 if Config.IsSurf then
820 ply.LastStartSpeed = ply.Tspeed
821 ply.Tspeed = nil
822 end
823
824 -- Check additional possibilities
825 if ply.Race then
826 ply.Race:Stop( ply )
827 end
828
829 -- Get the amount of points the user gets for completing the map
830 local InterpAverage = Timer.InterpolateAverage( nTime, ply.Style )
831 local InterpPoints = math.Round( Timer.GetPointsForMap( ply, nTime, ply.Style, InterpAverage, true ), 2 )
832
833 -- Notify the player
834 Player.Notification( ply, "BaseFinish", { Time = nTime, Difference = SelfDifference, Jumps = Jumps, Strafes = Strafes, Sync = CurrentSync, Points = IsImproved and InterpPoints } )
835
836 -- Check if they have an old record
837 local OldRecord = ply.Record or 0
838 if ply.Record != 0 and nTime >= ply.Record then return end
839
840 -- Update their stuff
841 ply.SpeedRequest = ply.Style
842 ply.Record = nTime
843 ply:SetObj( "Record", ply.Record )
844
845 -- Setup style variable
846 local PlayerStyle = ply.Style
847
848 -- Create a new object
849 local QueryTime, QueryObject = Timer.GetCurrentDate()
850
851 -- If we have something, update, otherwise, insert
852 if OldRecord > 0 then
853 QueryObject = Prepare(
854 "UPDATE game_times SET nTime = {0}, nDate = {1}, vData = NULL WHERE szMap = {2} AND szUID = {3} AND nStyle = {4}",
855 { nTime, QueryTime, Timer:GetMap(), ply.UID, PlayerStyle }
856 )
857 else
858 QueryObject = Prepare(
859 "INSERT INTO game_times VALUES ({0}, {1}, {2}, {3}, 0, {4}, NULL)",
860 { ply.UID, Timer:GetMap(), PlayerStyle, nTime, QueryTime }
861 )
862 end
863
864 -- Only proceed if we have valid object, proceed (which should be always, but OK)
865 if QueryObject then
866 QueryObject( AddRecord_Begin, { ply, nTime, OldRecord, QueryTime, PlayerStyle } )
867 else
868 print( "Records", "Something went wrong while adding time for", ply, nTime )
869 end
870end
871
872--[[
873 Description: Updates the run details if an existing entry exists
874 Used by: SQL callback
875--]]
876function Timer.UpdateRunDetails( data, varArg, szError )
877 local ply = varArg[ 1 ]
878
879 -- Make sure that nothing went wrong and update in the given position
880 if ply.SpeedPos and ply.SpeedPos > 0 and Records[ ply.Style ] and Records[ ply.Style ][ ply.SpeedPos ] and Records[ ply.Style ][ ply.SpeedPos ]["szUID"] == ply.UID then
881 Records[ ply.Style ][ ply.SpeedPos ]["vData"] = Core.Null( varArg[ 2 ] )
882 end
883
884 -- Reset speed pos
885 ply.SpeedPos = nil
886end
887
888--[[
889 Description: Checks if it's a valid request and inserts the details if possible
890 Used by: Client request
891--]]
892function Timer.SetRunDetails( ply, tab )
893 -- Since we're getting a response from the client, double-validate that it's actually legit
894 if ply.Record and ply.Record > 0 and ply.SpeedRequest then
895 -- Also check that we're not being cheeky
896 if ply.Practice or ply.TAS then
897 ply.SpeedRequest = nil
898 return
899 end
900
901 local function TabToString( tab )
902 -- Validate all fields
903 for i = 1, #tab do
904 if not tab[ i ] then
905 tab[ i ] = 0
906 end
907 end
908
909 -- Concatenate the whole table and clean up
910 local str = string.Implode( " ", tab )
911 Core.CleanTable( tab )
912
913 return str
914 end
915
916 -- Assemble all data into a simple string (Top speed, average speed, jumps, strafes, sync)
917 local tabData = { FL( Config.IsBhop and tab[ 1 ] or ply.LastStartSpeed or 0 ), FL( tab[ 2 ] ), ply.LastJumps, ply.LastStrafes, ply.LastSync }
918
919 -- Style specific extras
920 if ScrollStyles[ ply.SpeedRequest ] then
921 tabData[ 6 ] = ply.LastRatio
922 end
923
924 -- Create a writable entry
925 local szData = TabToString( tabData )
926
927 -- Update the vData column with all collected data
928 Prepare(
929 "UPDATE game_times SET vData = {0} WHERE szUID = {1} AND szMap = {2} AND nStyle = {3}",
930 { szData, ply.UID, Timer:GetMap(), ply.SpeedRequest }
931 )(
932 Timer.UpdateRunDetails,
933 { ply, szData }
934 )
935
936 -- Reset their speed request
937 ply.SpeedRequest = nil
938 end
939end
940Core.Register( "Global/Details", Timer.SetRunDetails )
941
942--[[
943 Description: Sends the top times table
944 Used by: Player connections
945--]]
946function Timer.SendTopTimes( ply )
947 local ar = NetPrepare( "Timer/Initial" )
948 ar:UInt( Core.CountHash( TopTimes ), 8 )
949
950 for s,t in pairs( TopTimes ) do
951 ar:UInt( s, 8 )
952 ar:Double( t )
953 end
954
955 if ply then
956 ar:Send( ply )
957 else
958 ar:Broadcast()
959 end
960end
961Core.SendTopTimes = Timer.SendTopTimes
962
963--[[
964 Description: Forces a full recalculation on the database end
965 Used by: Map cleanup, admin panel
966--]]
967function Timer.RecalculatePoints()
968 local szMap = Timer:GetMap()
969
970 for nStyle,_ in pairs( Records ) do
971 local nMultiplier = Timer:GetMultiplier( nStyle )
972 local nFourth, nDouble = nMultiplier / 4, nMultiplier * 2
973
974 sql.Query( "UPDATE game_times SET nPoints = " .. nMultiplier .. " * (" .. GetAverage( nStyle ) .. " / nTime) WHERE szMap = '" .. szMap .. "' AND nStyle = " .. nStyle )
975 sql.Query( "UPDATE game_times SET nPoints = " .. nDouble .. " WHERE szMap = '" .. szMap .. "' AND nStyle = " .. nStyle .. " AND nPoints > " .. nDouble )
976 sql.Query( "UPDATE game_times SET nPoints = " .. nFourth .. " WHERE szMap = '" .. szMap .. "' AND nStyle = " .. nStyle .. " AND nPoints < " .. nFourth )
977 end
978end
979Core.RecalculatePoints = Timer.RecalculatePoints
980
981--[[
982 Description: Updates the top time in the local table
983--]]
984function Timer.ChangeTopTime( nStyle, bGet, bAvoid )
985 -- Check if the time is valid
986 if Records[ nStyle ] and Records[ nStyle ][ 1 ] and Records[ nStyle ][ 1 ]["nTime"] then
987 -- Insert it into the TopTimes cache
988 TopTimes[ nStyle ] = Records[ nStyle ][ 1 ]["nTime"]
989
990 -- Return it if we want to get it
991 if bGet then
992 return TopTimes[ nStyle ] or 0, Records[ nStyle ][ 1 ]["szUID"]
993 end
994 end
995
996 -- Otherwise broadcast
997 if not bGet and not bAvoid then
998 Timer.SendTopTimes()
999 end
1000end
1001
1002--[[
1003 Description: Returns the multiplier for a given style
1004 Notes: All styles that follow the main course are given the base multiplier
1005--]]
1006function Timer:GetMultiplier( nStyle, bAll )
1007 if Core.IsValidBonus( nStyle ) then
1008 if type( self.BonusMultiplier ) == "table" then
1009 if bAll then
1010 local total = 0
1011 for i = 1, #self.BonusMultiplier do
1012 total = total + self.BonusMultiplier[ i ]
1013 end
1014 return total
1015 else
1016 return self.BonusMultiplier[ nStyle - Styles.Bonus + 1 ] or 0
1017 end
1018 else
1019 return self.BonusMultiplier
1020 end
1021 else
1022 return self.Multiplier
1023 end
1024end
1025
1026--[[
1027 Description: Gets the amount of points you would have for a specific time on a style
1028--]]
1029function Timer.GetPointsForMap( ply, nTime, nStyle, nAverage, bSingle )
1030 local total = 0
1031 if Core.IsValidBonus( nStyle ) and not bSingle then
1032 local ids = Zones.GetBonusIDs()
1033 for i = 1, #ids do
1034 local style = Styles.Bonus + ids[ i ]
1035 local rec = Timer.GetPlayerRecord( ply, style )
1036 if rec == 0 then continue end
1037
1038 local m = Timer:GetMultiplier( style )
1039 local p = m * (GetAverage( style ) / rec)
1040
1041 if p > m * 2 then p = m * 2
1042 elseif p < m / 4 then p = m / 4
1043 end
1044
1045 total = total + p
1046 end
1047 else
1048 if nTime == 0 then return 0 end
1049 if not nAverage then nAverage = GetAverage( nStyle ) end
1050
1051 local m = Timer:GetMultiplier( nStyle )
1052 total = m * (nAverage / nTime)
1053
1054 if total > m * 2 then total = m * 2
1055 elseif total < m / 4 then total = m / 4
1056 end
1057 end
1058
1059 return total
1060end
1061Core.GetPointsForMap = Timer.GetPointsForMap
1062
1063--[[
1064 Description: Gets the record ID you would have for a time
1065 Used by: Bots to show what record they're displaying
1066 Notes: Requested by Yeckoh on Surfline
1067--]]
1068function Timer.GetRecordID( nTime, nStyle )
1069 if Records[ nStyle ] then
1070 for i = 1, #Records[ nStyle ] do
1071 if nTime <= Records[ nStyle ][ i ]["nTime"] then
1072 return i
1073 end
1074 end
1075
1076 return #Records[ nStyle ] + 1
1077 else
1078 return 1
1079 end
1080end
1081Core.GetRecordID = Timer.GetRecordID
1082
1083--[[
1084 Description: Gets the steam ID of the player at the given position
1085 Used by: Profile command
1086--]]
1087function Timer.GetSteamAtID( nStyle, nID )
1088 if Records[ nStyle ] then
1089 for i = 1, #Records[ nStyle ] do
1090 if i == nID then
1091 return Records[ nStyle ][ i ]["szUID"]
1092 end
1093 end
1094 end
1095end
1096Core.GetSteamAtID = Timer.GetSteamAtID
1097
1098--[[
1099 Description: Gets the record entry for a player currently in the table
1100--]]
1101function Timer.GetPlayerRecord( ply, nOverride )
1102 -- Set base variables
1103 local nStyle, szSteam = nOverride or ply.Style, ply.UID
1104
1105 -- Check if we even have records for that style
1106 if Records[ nStyle ] then
1107 for i = 1, #Records[ nStyle ] do
1108 if Records[ nStyle ][ i ]["szUID"] == szSteam then
1109 return Records[ nStyle ][ i ]["nTime"], i
1110 end
1111 end
1112 end
1113
1114 return 0, 0
1115end
1116Core.GetPlayerRecord = Timer.GetPlayerRecord
1117
1118--[[
1119 Description: Gets the top X steam IDs
1120 Used by: Player medal setting
1121--]]
1122function Timer.GetTopSteam( nStyle, nAmount )
1123 local list = {}
1124 if Records[ nStyle ] then
1125 for i = 1, nAmount do
1126 if Records[ nStyle ][ i ] then
1127 list[ i ] = Records[ nStyle ][ i ]["szUID"]
1128 end
1129 end
1130 end
1131
1132 return list
1133end
1134
1135--[[
1136 Description: Gets the WR count on the player
1137--]]
1138function Timer.GetPlayerWRs( uid, style, all )
1139 local out = { 0, 0, 0 }
1140
1141 if style and Core.IsValidBonus( style ) then
1142 style = Styles.Bonus
1143 end
1144
1145 for _,data in pairs( WRTopCache[ uid ] or {} ) do
1146 local ts = data.nStyle
1147 if ts then
1148 out[ 1 ] = out[ 1 ] + 1
1149
1150 if Core.IsValidBonus( ts ) then
1151 ts = Styles.Bonus
1152 end
1153
1154 if ts == style then
1155 out[ 2 ] = out[ 2 ] + 1
1156 else
1157 out[ 3 ] = out[ 3 ] + 1
1158 end
1159
1160 if all then
1161 if not out.Rest then out.Rest = {} end
1162 out.Rest[ ts ] = (out.Rest[ ts ] or 0) + 1
1163 end
1164 end
1165 end
1166
1167 return out
1168end
1169
1170--[[
1171 Description: Approximate points gained for map
1172 Notes: It's called Interpolate because it isn't exact, but very accurate
1173--]]
1174function Timer.InterpolateAverage( nTime, nStyle )
1175 local nTotal, nCount, nLast = 0, 0, 0
1176
1177 -- Go through the top 50 and create an average from that
1178 for i = 1, 50 do
1179 if Records[ nStyle ] and Records[ nStyle ][ i ] then
1180 local nRec = Records[ nStyle ][ i ]["nTime"]
1181 nTotal = nTotal + nRec
1182 nCount = nCount + 1
1183 nLast = i
1184 else
1185 break
1186 end
1187 end
1188
1189 -- Remove the lowest time and replace it with our (fictional) time
1190 if nLast > 0 and Records[ nStyle ] and Records[ nStyle ][ nLast ]["nTime"] > nTime then
1191 nTotal = nTotal - Records[ nStyle ][ nLast ]["nTime"]
1192 nTotal = nTotal + nTime
1193 elseif nLast == 0 and nCount == 0 then
1194 nTotal = nTime
1195 nCount = 1
1196 end
1197
1198 -- Make sure we don't return a NaN
1199 if nCount == 0 then
1200 return 0
1201 else
1202 return nTotal / nCount
1203 end
1204end
1205
1206--[[
1207 Description: Opens the WR list for any other map
1208 Notes: EVEN for when they entered the current map
1209--]]
1210function Timer.DoRemoteWRList( ply, szMap, nStyle, nUpdate )
1211 if not szMap then return end
1212 if tonumber( szMap ) then
1213 local nID = tonumber( szMap )
1214 local nLim = Core.GetRecordCount( nStyle )
1215
1216 if nID <= 0 or nID > nLim then
1217 return Core.Print( ply, "General", Core.Text( "CommandWRListReach", nID, nLim ) )
1218 end
1219
1220 local nBottom = math.floor( (nID - 1) / Config.PageSize ) * Config.PageSize + 1
1221 local nTop = nBottom + Config.PageSize - 1
1222
1223 if nTop > nLim then
1224 nTop = nLim
1225 end
1226
1227 local args = { Core.GetRecordList( nStyle, nBottom, nTop ), nLim, nStyle }
1228 args.Started = nBottom
1229 args.TargetID = nID
1230
1231 return GAMEMODE:ShowSpare2( ply, args )
1232 end
1233
1234 if szMap == Timer:GetMap() and not ply.OutputSock then
1235 return GAMEMODE:ShowSpare2( ply, nil, nStyle )
1236 end
1237
1238 local SendData = {}
1239 local SendCount = 0
1240
1241 local WRMap = WRCache[ szMap ]
1242 if not WRMap or (type( WRMap ) == "table" and not WRMap[ nStyle ]) then
1243 if RTV.MapExists( szMap ) then
1244 if not WRMap then
1245 WRCache[ szMap ] = {}
1246 end
1247
1248 WRCache[ szMap ][ nStyle ] = {}
1249
1250 -- Request the data
1251 Prepare(
1252 "SELECT * FROM game_times WHERE szMap = {0} AND nStyle = {1} ORDER BY nTime ASC",
1253 { szMap, nStyle },
1254 nil, true
1255 )( function( data, varArg, szError )
1256 if Core.Assert( data, "szUID" ) then
1257 local makeNum, makeNull, nCount = tonumber, Core.Null, 1
1258 for j = 1, #data do
1259 data[ j ]["szMap"] = nil
1260 data[ j ]["nStyle"] = nil
1261 data[ j ]["nTime"] = makeNum( data[ j ]["nTime"] )
1262 data[ j ]["nPoints"] = makeNum( data[ j ]["nPoints"] )
1263 data[ j ]["nDate"] = makeNum( data[ j ]["nDate"] ) or 0
1264 data[ j ]["vData"] = makeNull( data[ j ]["vData"] )
1265
1266 WRCache[ szMap ][ nStyle ][ nCount ] = data[ j ]
1267 nCount = nCount + 1
1268 end
1269 end
1270 end )
1271
1272 if ply.OutputFull then
1273 return WRCache[ szMap ][ nStyle ], "Full"
1274 end
1275
1276 local nStart, nMaximum = 1, Config.PageSize
1277 if nUpdate then
1278 nStart, nMaximum = nUpdate[ 1 ], nUpdate[ 2 ]
1279 end
1280
1281 for i = nStart, nMaximum do
1282 if WRCache[ szMap ][ nStyle ][ i ] then
1283 SendData[ i ] = WRCache[ szMap ][ nStyle ][ i ]
1284 end
1285 end
1286
1287 SendCount = #WRCache[ szMap ][ nStyle ]
1288 else
1289 return Core.Print( ply, "General", Core.Text( "MapInavailable", szMap ) )
1290 end
1291 else
1292 -- This means we already fetched the data
1293 local nStart, nMaximum = 1, Config.PageSize
1294 if nUpdate then
1295 nStart, nMaximum = nUpdate[ 1 ], nUpdate[ 2 ]
1296 end
1297
1298 for i = nStart, nMaximum do
1299 if WRCache[ szMap ][ nStyle ][ i ] then
1300 SendData[ i ] = WRCache[ szMap ][ nStyle ][ i ]
1301 end
1302 end
1303
1304 SendCount = #WRCache[ szMap ][ nStyle ]
1305 end
1306
1307 -- Scan for data
1308 local bZero = true
1309 for i,data in pairs( SendData ) do
1310 if i and data then bZero = false break end
1311 end
1312
1313 -- If we don't have anything, show only a print
1314 if bZero or SendCount == 0 then
1315 if nUpdate then return end
1316 return Core.Print( ply, "Timer", Core.Text( "CommandRemoteWRListBlank", szMap, Core.StyleName( nStyle ) ) )
1317 else
1318 if ply.OutputSock then
1319 return SendData, SendCount
1320 end
1321
1322 if nUpdate then
1323 NetPrepare( "GUI/Update", {
1324 ID = "Records",
1325 Data = { SendData, SendCount }
1326 } ):Send( ply )
1327 else
1328 NetPrepare( "GUI/Build", {
1329 ID = "Records",
1330 Title = "Server records",
1331 X = 500,
1332 Y = 400,
1333 Mouse = true,
1334 Blur = true,
1335 Data = { SendData, SendCount, nStyle, IsEdit = ply.RemovingTimes, Map = szMap }
1336 } ):Send( ply )
1337 end
1338 end
1339end
1340Core.DoRemoteWR = Timer.DoRemoteWRList
1341
1342--[[
1343 Description: Responds with an update to the request
1344 Notes: Much more efficient than what I used to do with paging
1345--]]
1346function Timer.WRListRequest( ply, varArgs )
1347 local nStyle = varArgs[ 1 ]
1348 local tabOffset = varArgs[ 2 ]
1349 local szMap = varArgs[ 3 ]
1350
1351 -- If a map is provided, send a remote WR update
1352 if szMap then
1353 Timer.DoRemoteWRList( ply, szMap, nStyle, tabOffset )
1354 else
1355 NetPrepare( "GUI/Update", {
1356 ID = "Records",
1357 Data = { Core.GetRecordList( nStyle, tabOffset[ 1 ], tabOffset[ 2 ] ), Core.GetRecordCount( nStyle ) }
1358 } ):Send( ply )
1359 end
1360end
1361Core.Register( "Global/RetrieveList", Timer.WRListRequest )
1362
1363--[[
1364 Description: Removes times by request of an admin
1365 Notes: Migrated from the admin panel to here
1366--]]
1367function Timer.RemoveListRequest( ply, varArgs )
1368 -- Not that people will, but people might
1369 if not ply.RemovingTimes then
1370 return Core.Print( ply, "Admin", Core.Text( "MiscIllegalAccess" ) )
1371 end
1372
1373 local nStyle = tonumber( varArgs[ 1 ] )
1374 local tabContent = varArgs[ 2 ]
1375 local szMap = varArgs[ 3 ]
1376 local nView = tonumber( varArgs[ 4 ] )
1377
1378 if nView then
1379 if nView == 1 then
1380 Core.RemoveRaceItems( ply, nStyle, szMap )
1381 elseif nView == 2 then
1382 Core.RemoveStageTimes( ply, nStyle, tonumber( tabContent ), szMap )
1383 elseif nView == 4 then
1384 Core.RemoveTASTimes( ply, nStyle, szMap )
1385 elseif nView == 8 then
1386 Core.RemoveStatsItems( ply, nStyle, szMap )
1387 end
1388
1389 return
1390 end
1391
1392 if not szMap then
1393 szMap = Timer:GetMap()
1394 end
1395
1396 -- Delete the times
1397 local nAmount, bLocal, bMain, bHistory = 0, szMap == Timer:GetMap()
1398 local info = Core.GetBotInfo( nStyle )
1399
1400 for i = 1, #tabContent do
1401 sql.Query( "DELETE FROM game_times WHERE szMap = '" .. szMap .. "' AND nStyle = " .. nStyle .. " AND szUID = '" .. tabContent[ i ].szUID .. "'" )
1402 nAmount = nAmount + 1
1403
1404 if bLocal and info and info.Style == nStyle and info.SteamID == tabContent[ i ].szUID and info.Time == tabContent[ i ].nTime then
1405 bMain = true
1406 sql.Query( "DELETE FROM game_bots WHERE szMap = '" .. szMap .. "' AND nStyle = " .. info.Style .. " AND szSteam = '" .. info.SteamID .. "'" )
1407
1408 local szStyle = nStyle == Styles.Normal and ".txt" or ("_" .. nStyle .. ".txt")
1409 if file.Exists( Config.BaseType .. "/bots/bot_" .. szMap .. szStyle, "DATA" ) then
1410 file.Delete( Config.BaseType .. "/bots/bot_" .. szMap .. szStyle )
1411 end
1412
1413 local bot = Core.GetBot( nil, nStyle == Styles.Normal and "Main" or "Multi" )
1414 if IsValid( bot ) and bot.Style == nStyle then
1415 Core.ClearBot( bot, nStyle )
1416 end
1417 end
1418 end
1419
1420 -- Check if it's the local map and not remote
1421 if bLocal then
1422 -- If local, reload everything
1423 Core.LoadRecords()
1424
1425 -- Scan history runs
1426 local runs = Core.LoadBotHistory( nStyle )
1427 for i = 1, #tabContent do
1428 for j = 1, #runs do
1429 -- Required to limit this since the util.TableToJSON can't handle more decimals...
1430 local limited = tonumber( string.format( "%.4f", tabContent[ i ].nTime ) )
1431 if runs[ j ].Style == nStyle and runs[ j ].SteamID == tabContent[ i ].szUID and runs[ j ].Time == limited then
1432 if file.Exists( runs[ j ].FilePath, "DATA" ) then
1433 file.Delete( runs[ j ].FilePath )
1434 bHistory = true
1435
1436 local str = runs[ j ].FilePath
1437 local index = str:match( "^.*()_" )
1438 local id = tonumber( string.match( string.sub( str, index + 1, #str ), "%d+" ) ) + 1
1439 local base = string.sub( str, 1, index ) .. "v"
1440
1441 -- Find all existing files
1442 while file.Exists( base .. id .. ".txt", "DATA" ) do
1443 file.Write( base .. (id - 1) .. ".txt", file.Read( base .. id .. ".txt", "DATA" ) )
1444 file.Delete( base .. id .. ".txt" )
1445 id = id + 1
1446 end
1447 end
1448 end
1449 end
1450 end
1451
1452 for _,p in pairs( player.GetHumans() ) do
1453 for i = 1, #tabContent do
1454 -- Only reload their time if they match the style
1455 if p.UID == tabContent[ i ].szUID and p.Style == nStyle then
1456 p:LoadTime( true )
1457 end
1458 end
1459 end
1460 end
1461
1462 local info, str = {}, ""
1463 if bLocal then info[ #info + 1 ] = "All records have been reloaded" end
1464 if bHistory then info[ #info + 1 ] = "History bot deleted" end
1465 if bMain then info[ #info + 1 ] = "Main bot deleted" end
1466 if #info > 0 then str = "[" .. string.Implode( "; ", info ) .. "]" end
1467
1468 Core.Print( ply, "Admin", Core.Text( "AdminTimesRemoved", nAmount, str ) )
1469 Core.AddAdminLog( "Removed " .. nAmount .. " times on " .. Timer:GetMap() .. " (" .. Core.StyleName( nStyle ) .. ", " .. str .. ")", ply.UID, ply:Name() )
1470end
1471Core.Register( "Global/RemoveList", Timer.RemoveListRequest )
1472
1473--[[
1474 Description: Sends the appropriate list to the player
1475 Notes: Houses Maps Left, Beat and My WR
1476--]]
1477function Core.HandlePlayerMaps( szID, ply, args )
1478 local nStyle, szUID = ply.Style
1479 if args and #args > 0 then
1480 if util.SteamIDTo64( args.Upper[ 1 ] ) != "0" then
1481 szUID = args.Upper[ 1 ]
1482 elseif szID == "Beat" or szID == "Left" then
1483 local szStyle = string.Implode( " ", args.Upper )
1484 local nGet = Core.GetStyleID( szStyle )
1485
1486 if not Core.IsValidStyle( nGet ) then
1487 return Core.Print( ply, "General", Core.Text( "MiscInvalidStyle" ) )
1488 else
1489 nStyle = nGet
1490 end
1491 end
1492 end
1493
1494 local IsRemote = szUID and szUID != ply.UID
1495 szUID = szUID or ply.UID
1496
1497 if not BeatMaps[ szUID ] then
1498 BeatMaps[ szUID ] = {}
1499 end
1500
1501 if szID == "Beat" or szID == "Left" or szID == "NoWR" then
1502 if not BeatMaps[ szUID ][ nStyle ] then
1503 BeatMaps[ szUID ][ nStyle ] = true
1504
1505 Prepare(
1506 "SELECT szMap, nTime, nPoints, nDate FROM game_times WHERE szUID = {0} AND nStyle = {1} ORDER BY nPoints ASC",
1507 { szUID, nStyle },
1508 nil, true
1509 )( function( data, varArg, szError )
1510 if Core.Assert( data, "szMap" ) then
1511 local makeNum = tonumber
1512 for j = 1, #data do
1513 data[ j ]["nTime"] = makeNum( data[ j ]["nTime"] )
1514 data[ j ]["nPoints"] = makeNum( data[ j ]["nPoints"] )
1515 data[ j ]["nDate"] = makeNum( data[ j ]["nDate"] )
1516 end
1517
1518 BeatMaps[ szUID ][ nStyle ] = data
1519 end
1520 end )
1521 end
1522
1523 local count = BeatMaps[ szUID ][ nStyle ] and type( BeatMaps[ szUID ][ nStyle ] ) == "table" and #BeatMaps[ szUID ][ nStyle ] or -1
1524 if args and args.GetCount then
1525 if count < 0 then
1526 count = 0
1527 end
1528
1529 return count
1530 end
1531
1532 if szID == "NoWR" then
1533 if count <= 0 then
1534 return Core.Print( ply, "General", Core.Text( "CommandNoWRBeat", IsRemote and "This player hasn't" or "You haven't" ) )
1535 end
1536
1537 local data, tab, cache = {}, BeatMaps[ szUID ][ nStyle ], WRTopCache[ szUID ] or {}
1538 for i = 1, #cache do
1539 for j = 1, count do
1540 if cache[ i ].nTime == tab[ j ].nTime then
1541 data[ #data + 1 ] = cache[ i ]
1542 end
1543 end
1544 end
1545
1546 if #data == 0 then
1547 return Core.Print( ply, "General", Core.Text( "CommandNoWRNone", IsRemote and "This player doesn't" or "You don't" ) )
1548 elseif #data == Timer.Maps then
1549 return Core.Print( ply, "General", Core.Text( "CommandNoWRAll" .. (IsRemote and "Remote" or "") ) )
1550 end
1551
1552 NetPrepare( "GUI/Build", {
1553 ID = "Maps",
1554 Title = "No WR maps on " .. Core.StyleName( nStyle ),
1555 X = 400,
1556 Y = 390,
1557 Mouse = true,
1558 Blur = true,
1559 Data = { data, Style = -1, Type = szID, Version = Core.GetMaplistVersion() }
1560 } ):Send( ply )
1561
1562 return false
1563 end
1564
1565 if count < 0 then
1566 return Core.Print( ply, "General", szID == "Left" and Core.Text( "CommandWRLeftNone", IsRemote and "This player still needs" or "You still need" ) or Core.Text( "CommandWRBeatNone", IsRemote and "This player hasn't" or "You haven't" ) )
1567 elseif szID == "Left" and count == Timer.Maps then
1568 return Core.Print( ply, "General", Core.Text( "CommandWRLeftAll", IsRemote and "This player has" or "You have" ) )
1569 elseif szID == "Beat" and count == 0 then
1570 return Core.Print( ply, "General", Core.Text( "CommandWRLeftNone", IsRemote and "This player still needs" or "You still need" ) )
1571 end
1572
1573 if count > 0 and count <= Timer.Maps then
1574 NetPrepare( "GUI/Build", {
1575 ID = "Maps",
1576 Title = "Maps " .. szID,
1577 X = 400 + (szID == "Beat" and 100 or 0),
1578 Y = 390,
1579 Mouse = true,
1580 Blur = true,
1581 Data = { BeatMaps[ szUID ][ nStyle ], Style = nStyle, Type = szID, Version = Core.GetMaplistVersion() }
1582 } ):Send( ply )
1583 else
1584 Core.Print( ply, "General", Core.Text( "CommandWRListUnable", IsRemote and "the player hasn't" or "you haven't" ) )
1585 end
1586 elseif szID == "Mine" then
1587 if not BeatMaps[ szUID ][ 0 ] then
1588 BeatMaps[ szUID ][ 0 ] = true
1589
1590 if WRTopCache[ szUID ] then
1591 BeatMaps[ szUID ][ 0 ] = WRTopCache[ szUID ]
1592 end
1593 end
1594
1595 local count = BeatMaps[ szUID ][ 0 ] and type( BeatMaps[ szUID ][ 0 ] ) == "table" and #BeatMaps[ szUID ][ 0 ] or 0
1596 if count > 0 then
1597 NetPrepare( "GUI/Build", {
1598 ID = "Maps",
1599 Title = "#1 WRs (" .. count .. ")",
1600 X = 400,
1601 Y = 390,
1602 Mouse = true,
1603 Blur = true,
1604 Data = { BeatMaps[ szUID ][ 0 ], Style = 0, By = IsRemote and szUID }
1605 } ):Send( ply )
1606 else
1607 Core.Print( ply, "General", (IsRemote and "This player doesn't" or "You don't") .. " seem to have any #1 records" )
1608 end
1609 end
1610end
1611
1612--[[
1613 Description: Get the keys pressed at the moment of request
1614 Notes: Used to see what keys to press when returning to a checkpoint with high velocity
1615--]]
1616function Timer.GetCheckpointKeys( ply )
1617 local szStr = ply:Crouching() and " C" or ""
1618 if ply:KeyDown( IN_MOVELEFT ) then
1619 szStr = szStr .. " A"
1620 elseif ply:KeyDown( IN_MOVERIGHT ) then
1621 szStr = szStr .. " D"
1622 end
1623
1624 return szStr
1625end
1626
1627--[[
1628 Description: The checkpoint request processing
1629--]]
1630function Timer.CheckpointRequest( ply, varArgs, IsForce )
1631 if not ply.Practice then return Core.Print( ply, "General", Core.Text( "TimerCheckpointMenuPractice" ) ) end
1632 if ply.CheckpointTeleport then return Core.Print( ply, "Timer", Core.Text( "TimerCheckpointWaiting" ) ) end
1633
1634 local ID = varArgs[ 1 ]
1635 local IsDelay = varArgs[ 2 ]
1636 local IsDelete = varArgs[ 3 ]
1637 local IsWipe = varArgs[ 4 ]
1638 local IsFixedWrite = varArgs[ 5 ]
1639 local CanSave = true
1640 local Send = {}
1641
1642 if not Checkpoints[ ply ] then
1643 Checkpoints[ ply ] = {}
1644 end
1645
1646 -- This means load last loaded / load last saved
1647 if ID == 1 then
1648 ID = ply.LastCP
1649 CanSave, IsDelete, IsWipe = nil, nil, nil
1650 elseif ID == 2 then
1651 ID = ply.LastWriteCP
1652 CanSave, IsDelete, IsWipe = nil, nil, nil
1653 end
1654
1655 -- Check if we're force writing
1656 if IsForce then
1657 local WriteAt = 3
1658 for i = 3, 9 do
1659 if not Checkpoints[ ply ][ i ] then
1660 WriteAt = i
1661 break
1662 end
1663 end
1664
1665 ID = WriteAt
1666 Checkpoints[ ply ][ ID ] = nil
1667
1668 -- Else find a valid ID
1669 elseif not ID then
1670 local IsAny
1671 for i = 3, 9 do
1672 if Checkpoints[ ply ][ i ] then
1673 IsAny = i
1674 break
1675 elseif IsFixedWrite then
1676 IsAny = i
1677 break
1678 end
1679 end
1680
1681 if IsAny then
1682 ID = ply.LastCP or IsAny
1683
1684 if not IsFixedWrite and (not ID or not Checkpoints[ ply ][ ID ]) then
1685 return Core.Print( ply, "Timer", Core.Text( "TimerCheckpointMissing" ) )
1686 end
1687 else
1688 return Core.Print( ply, "Timer", Core.Text( "TimerCheckpointLoadBlank" ) )
1689 end
1690 end
1691
1692 if ID and IsFixedWrite then
1693 CanSave = true
1694 end
1695
1696 -- If we have a checkpoint
1697 if Checkpoints[ ply ][ ID ] and not IsFixedWrite then
1698 if IsDelete then
1699 Checkpoints[ ply ][ ID ] = nil
1700 Send.Type = "Delete"
1701 Send.ID = ID
1702 elseif IsWipe then
1703 -- Iterate over all checkpoints
1704 for i = 3, 9 do
1705 Checkpoints[ ply ][ i ] = nil
1706 end
1707
1708 -- Reset variables
1709 ply.LastCP = nil
1710 ply.LastWriteCP = nil
1711
1712 Send.Type = "Wipe"
1713 else
1714 ply.LastCP = ID
1715
1716 -- Setup the function
1717 local function MakeTeleport()
1718 if not IsValid( ply ) then return end
1719 if not ply.Practice or ply.Spectating then
1720 return Core.Print( ply, "General", Core.Text( "TimerCheckpointPractice" ) )
1721 end
1722
1723 ply.CheckpointTeleport = nil
1724
1725 local cp = Checkpoints[ ply ][ ply.LastCP ]
1726 ply:SetPos( cp[ 1 ] )
1727 ply:SetEyeAngles( cp[ 2 ] )
1728 ply:SetLocalVelocity( cp[ 3 ] )
1729 end
1730
1731 if IsDelay then
1732 ply.CheckpointTeleport = true
1733 Send.Type = "Delay"
1734
1735 timer.Simple( 1.5, MakeTeleport )
1736 else
1737 MakeTeleport()
1738 end
1739 end
1740 elseif CanSave then
1741 if IsDelete then
1742 Core.Print( ply, "Timer", Core.Text( "TimerCheckpointBlank" ) )
1743 elseif IsWipe then
1744 -- Iterate over all checkpoints
1745 for i = 3, 9 do
1746 Checkpoints[ ply ][ i ] = nil
1747 end
1748
1749 -- Reset variables
1750 ply.LastCP = nil
1751 ply.LastWriteCP = nil
1752
1753 Send.Type = "Wipe"
1754 else
1755 local pos, ang, vel = ply:GetPos(), ply:EyeAngles(), ply:GetVelocity()
1756 if ply.Spectating and IsValid( ply:GetObserverTarget() ) then
1757 local target = ply:GetObserverTarget()
1758 pos = target:GetPos()
1759 ang = target:EyeAngles()
1760 vel = target:GetVelocity()
1761 end
1762
1763 Checkpoints[ ply ][ ID ] = { pos, ang, vel, ST() }
1764 Send.Type = "Add"
1765 Send.ID = ID
1766 Send.Details = string.format( "%.0f u/s%s", Checkpoints[ ply ][ ID ][ 3 ]:Length2D(), Timer.GetCheckpointKeys( ply ) )
1767
1768 ply.LastWriteCP = ID
1769 end
1770 else
1771 return Core.Print( ply, "Timer", Core.Text( "TimerCheckpointLoadBlank" ) )
1772 end
1773
1774 if Send.Type then
1775 Core.Send( ply, "GUI/UpdateCP", Send )
1776 end
1777end
1778Core.Register( "Global/Checkpoints", Timer.CheckpointRequest )
1779
1780--[[
1781 Description: Handles the checkpoint commands
1782--]]
1783function Timer.CheckpointCommand( ply, args )
1784 -- Check if they're in practice mode or not
1785 if not ply.Practice and args.Key != "cphelp" then
1786 return Core.Print( ply, "General", Core.Text( "TimerCheckpointMenuPractice" ) )
1787 end
1788
1789 -- Allocate them a spot in the checkpoint table
1790 if not Checkpoints[ ply ] then
1791 Checkpoints[ ply ] = {}
1792 end
1793
1794 if args.Key == "cp" or args.Key == "cpmenu" then
1795 Core.Send( ply, "GUI/Create", { ID = "Checkpoints", Dimension = { x = 200, y = 332, px = 20 }, Args = { Title = "Checkpoint Menu" } } )
1796 elseif args.Key == "cpload" then
1797 Timer.CheckpointRequest( ply, {} )
1798 elseif args.Key == "cpsave" then
1799 Timer.CheckpointRequest( ply, {}, true )
1800 elseif args.Key == "cpset" then
1801 local ID = tonumber( args[ 1 ] )
1802 if not ID or ID < 3 or ID > 9 then
1803 Core.Print( ply, "Timer", Core.Text( "TimerCheckpointInvalidID" ) )
1804 else
1805 ply.LastCP = ID
1806 Core.Print( ply, "Timer", Core.Text( "TimerCheckpointManualSet", ID ) )
1807 end
1808 elseif args.Key == "cpwipe" or args.Key == "cpdelete" then
1809 local id
1810 if #args > 0 then
1811 id = tonumber( args[ 1 ] )
1812 end
1813
1814 -- Iterate over all checkpoints
1815 for i = 3, 9 do
1816 if id == nil or id == i then
1817 Checkpoints[ ply ][ i ] = nil
1818 end
1819 end
1820
1821 -- Reset variables
1822 if not id then
1823 ply.LastCP = nil
1824 ply.LastWriteCP = nil
1825 end
1826
1827 Core.Send( ply, "GUI/UpdateCP", { Type = "Wipe", ID = id } )
1828 elseif args.Key == "cphelp" then
1829 Core.Print( ply, "General", Core.Text( "TimerCheckpointHelp" ) )
1830 end
1831end
1832Core.AddCmd( { "cp", "cpmenu", "cpload", "cpsave", "cpset", "cphelp", "cpwipe", "cpdelete" }, Timer.CheckpointCommand )
1833
1834--[[
1835 Description: Loads everything we've got
1836 Used by: Gamemode initialization
1837--]]
1838function Core.LoadRecords()
1839 -- Clean up Maps table for if it's a reload
1840 Core.CleanTable( Maps )
1841
1842 -- Reset map count
1843 Timer.Maps = 0
1844
1845 -- Set the base statistics variable
1846 Timer.BaseStatistics = { 0, 0 }
1847
1848 -- Load all maps into the Maps table
1849 Prepare(
1850 "SELECT * FROM game_map ORDER BY szMap ASC",
1851 nil, nil, true
1852 )( function( data, varArg, szError )
1853 if Core.Assert( data, "szMap" ) then
1854 local makeNum, makeNull = tonumber, Core.Null
1855 for j = 1, #data do
1856 local map = data[ j ]["szMap"]
1857 data[ j ]["szMap"] = nil
1858 data[ j ]["nMultiplier"] = makeNum( makeNull( data[ j ]["nMultiplier"], 1 ) )
1859 data[ j ]["nBonusMultiplier"] = makeNull( data[ j ]["nBonusMultiplier"], 0 )
1860 data[ j ]["nPlays"] = makeNum( makeNull( data[ j ]["nPlays"], 0 ) )
1861 data[ j ]["nOptions"] = makeNum( makeNull( data[ j ]["nOptions"], 0 ) )
1862 data[ j ]["szDate"] = makeNull( data[ j ]["szDate"], "Unknown" )
1863
1864 -- Check the bonus multiplier
1865 if data[ j ]["nBonusMultiplier"] != 0 then
1866 local nNum = makeNum( data[ j ]["nBonusMultiplier"] )
1867 if not nNum and string.find( data[ j ]["nBonusMultiplier"], " " ) then
1868 local szNums = string.Explode( " ", data[ j ]["nBonusMultiplier"] )
1869 for i = 1, #szNums do
1870 if string.find( szNums[ i ], ":", 1, true ) then
1871 local szSplit = string.Explode( ":", szNums[ i ] )
1872 szNums[ i ] = { makeNum( szSplit[ 2 ] ) }
1873 else
1874 szNums[ i ] = makeNum( szNums[ i ] ) or 0
1875 end
1876 end
1877
1878 data[ j ]["nBonusMultiplier"] = szNums
1879 else
1880 data[ j ]["nBonusMultiplier"] = nNum or 0
1881 end
1882 else
1883 data[ j ]["nBonusMultiplier"] = makeNum( data[ j ]["nBonusMultiplier"] )
1884 end
1885
1886 -- Load tier and type for surf
1887 if Config.IsSurf then
1888 data[ j ]["nTier"] = makeNum( makeNull( data[ j ]["nTier"], 1 ) )
1889 data[ j ]["nType"] = makeNum( makeNull( data[ j ]["nType"], 0 ) )
1890 end
1891
1892 -- Add the map and increment
1893 Maps[ map ] = data[ j ]
1894 Timer.Maps = Timer.Maps + 1
1895
1896 if data[ j ]["nPlays"] > Timer.BaseStatistics[ 2 ] then
1897 Timer.BaseStatistics[ 2 ] = data[ j ]["nPlays"]
1898 Timer.BaseStatistics[ 3 ] = map
1899 end
1900 end
1901 end
1902 end )
1903
1904 -- Get the details for the current map
1905 local map = Timer:GetMap()
1906 if Maps[ map ] then
1907 Timer.Multiplier = Maps[ map ]["nMultiplier"] or 1
1908 Timer.BonusMultiplier = Maps[ map ]["nBonusMultiplier"] or 0
1909 Timer.Options = Maps[ map ]["nOptions"] or 0
1910 Timer.Plays = (Maps[ map ]["nPlays"] or 0) + 1
1911 Timer.Date = Maps[ map ]["szDate"] or ""
1912
1913 -- Surf details
1914 Timer.Tier = Maps[ map ]["nTier"] or 1
1915 Timer.Type = Maps[ map ]["nType"] or 0
1916 else
1917 Timer.Multiplier = 1
1918 Timer.BonusMultiplier = 0
1919 Timer.Options = 0
1920 Timer.Plays = 0
1921 Timer.Date = ""
1922
1923 -- Surf details
1924 Timer.Tier = 1
1925 Timer.Type = 0
1926 end
1927
1928 -- When we're dealing with a new map, update its date
1929 if Timer.IsNewMap then
1930 Timer.IsNewMap = nil
1931
1932 local szDate = Timer.GetCurrentDate( true )
1933 if Timer.Date == "" or Timer.Date == "Unknown" then
1934 Prepare( "UPDATE game_map SET szDate = {0} WHERE szMap = {1}", { szDate, map } )
1935 Timer.Date = szDate
1936
1937 if Maps[ map ] and Maps[ map ]["szDate"] then
1938 Maps[ map ]["szDate"] = szDate
1939 end
1940 end
1941
1942 return false
1943 end
1944
1945 -- Add a single play to the map
1946 if not Timer.IsLoaded then
1947 Prepare( "UPDATE game_map SET nPlays = nPlays + 1, szDate = {0} WHERE szMap = {1}", { Timer.GetCurrentDate( true ), map } )
1948 end
1949
1950 -- If the table was populated, clean out everything
1951 for _,v in pairs( Records ) do
1952 if v and type( v ) != "table" then continue end
1953 Core.CleanTable( v )
1954 end
1955
1956 -- Pre-prepare all styles
1957 local StyleCounter = {}
1958 for _,n in pairs( Styles ) do
1959 if not Records[ n ] then
1960 Records[ n ] = {}
1961 StyleCounter[ n ] = 1
1962 end
1963 end
1964
1965 -- Load all styles except for practice
1966 Prepare(
1967 "SELECT * FROM game_times WHERE szMap = {0} ORDER BY nTime ASC",
1968 { Timer:GetMap() },
1969 nil, true
1970 )( function( data, varArg, szError )
1971 if Core.Assert( data, "szUID" ) then
1972 local makeNum, makeNull, styleId = tonumber, Core.Null
1973 for j = 1, #data do
1974 styleId = makeNum( data[ j ]["nStyle"] )
1975 data[ j ]["szMap"] = nil
1976 data[ j ]["nStyle"] = nil
1977 data[ j ]["nTime"] = makeNum( data[ j ]["nTime"] )
1978 data[ j ]["nPoints"] = makeNum( data[ j ]["nPoints"] )
1979 data[ j ]["nDate"] = makeNum( data[ j ]["nDate"] ) or 0
1980 data[ j ]["vData"] = makeNull( data[ j ]["vData"] )
1981
1982 if not Records[ styleId ] then Records[ styleId ] = {} end
1983 if not StyleCounter[ styleId ] then StyleCounter[ styleId ] = 1 end
1984
1985 Records[ styleId ][ StyleCounter[ styleId ] ] = data[ j ]
1986 StyleCounter[ styleId ] = StyleCounter[ styleId ] + 1
1987 end
1988 end
1989 end )
1990
1991 if not Timer.IsLoaded then
1992 -- Set the statistics value
1993 for _,value in pairs( StyleCounter ) do
1994 Timer.BaseStatistics[ 1 ] = Timer.BaseStatistics[ 1 ] + value - 1
1995 end
1996
1997 -- Get the total amount of times on the server
1998 Prepare(
1999 "SELECT COUNT(nTime) AS nCount FROM game_times"
2000 )( function( data, varArg, szError )
2001 if Core.Assert( data, "nCount" ) then
2002 Timer.BaseStatistics[ 4 ] = tonumber( data[ 1 ]["nCount"] ) or 0
2003 end
2004 end )
2005
2006 -- Get command stats
2007 Timer.BaseStatistics[ 5 ], Timer.BaseStatistics[ 6 ] = Core.CountCommands()
2008 end
2009
2010 -- Set all the #1 times for sending
2011 for style,_ in pairs( StyleCounter ) do
2012 Timer.ChangeTopTime( style, nil, true )
2013 CalcAverage( style )
2014 end
2015
2016 -- Do the point sum caching
2017 if not Timer.IsLoaded then
2018 Timer.PlayerCount = {}
2019 Timer.PlayerLadderPos = {}
2020
2021 Prepare(
2022 "SELECT nStyle, szUID, SUM(nPoints) AS nSum FROM game_times WHERE szMap != {0} GROUP BY nStyle, szUID",
2023 { Timer:GetMap() },
2024 nil, true
2025 )( function( data, varArg, szError )
2026 local makeNum, out = tonumber, {}
2027 if Core.Assert( data, "nSum" ) then
2028 for j = 1, #data do
2029 local nStyle = makeNum( data[ j ]["nStyle"] )
2030 if not StylePoints[ nStyle ] then
2031 StylePoints[ nStyle ] = {}
2032 out[ nStyle ] = {}
2033 end
2034
2035 local pts = makeNum( data[ j ]["nSum"] ) or 0
2036 StylePoints[ nStyle ][ data[ j ]["szUID"] ] = pts
2037
2038 local into = out[ nStyle ]
2039 into[ #into + 1 ] = { UID = data[ j ]["szUID"], Pts = pts }
2040 end
2041 end
2042
2043 for style,data in pairs( out ) do
2044 table.SortByMember( data, "Pts" )
2045 Timer.PlayerLadderPos[ style ] = {}
2046
2047 for i = 1, #data do
2048 Timer.PlayerLadderPos[ style ][ data[ i ].UID ] = i
2049 end
2050 end
2051 end )
2052
2053 Prepare(
2054 "SELECT nStyle, COUNT(DISTINCT(szUID)) AS nAmount FROM game_times GROUP BY nStyle"
2055 )( function( data, varArg, szError )
2056 local makeNum = tonumber
2057 if Core.Assert( data, "nAmount" ) then
2058 for j = 1, #data do
2059 Timer.PlayerCount[ makeNum( data[ j ]["nStyle"] ) ] = makeNum( data[ j ]["nAmount"] ) or 0
2060 end
2061 end
2062 end )
2063 end
2064
2065 -- Loads all ranks and the top list
2066 Player:LoadRanks()
2067
2068 -- Only do these things on first load
2069 if not Timer.IsLoaded then
2070 -- Load the zones from the database
2071 Zones.Load()
2072
2073 -- Starts the RTV instance
2074 RTV:Start()
2075
2076 -- Enable all extensions
2077 for i = 1, #Timer.PostInitFunc do
2078 Timer.PostInitFunc[ i ]()
2079 end
2080 end
2081
2082 -- Set a variable to keep track of whether this has been loaded before
2083 Timer.IsLoaded = true
2084end
2085
2086
2087
2088
2089
2090-- Player class
2091Player.LadderScalar = 1.20
2092Player.TopListLimit = 50
2093
2094Player.MultiplierNormal = 1
2095Player.MultiplierBonus = 1
2096Player.MultiplierAngled = 1
2097
2098Player.NormalScalar = 0.0001
2099Player.BonusScalar = 0.0001
2100Player.AngledScalar = 0.0001
2101
2102Player.AveragePoints = 1
2103Player.AveragePointsCache = {}
2104Player.NotifyCache = {}
2105
2106
2107--[[
2108 Description:
2109 Used by: Timer start-up, initialization
2110--]]
2111function Player:LoadRanks()
2112 local NormalSum, BonusSum = 0, 0
2113
2114 -- Get the total sum of points
2115 for map,data in pairs( Maps ) do
2116 NormalSum = NormalSum + data["nMultiplier"]
2117
2118 if type( data["nBonusMultiplier"] ) == "table" then
2119 for i = 1, #data["nBonusMultiplier"] do
2120 if type( data["nBonusMultiplier"][ i ] ) == "table" then
2121 data["nBonusMultiplier"][ i ] = data["nBonusMultiplier"][ i ][ 1 ]
2122 else
2123 BonusSum = BonusSum + data["nBonusMultiplier"][ i ]
2124 end
2125 end
2126
2127 data["nBonusMultiplier"] = string.Implode( ", ", data["nBonusMultiplier"] )
2128 else
2129 BonusSum = BonusSum + data["nBonusMultiplier"]
2130 end
2131 end
2132
2133 -- If there's no maps, we still need to be able to calculate simple ranks
2134 if NormalSum == 0 then NormalSum = 1 end
2135 if BonusSum == 0 then BonusSum = 1 end
2136
2137 -- Set the multipliers
2138 self.MultiplierNormal = NormalSum
2139 self.MultiplierBonus = BonusSum
2140 self.MultiplierAngled = NormalSum / 2
2141
2142 -- Set some local functionality
2143 local mp, c = math.pow, #Ranks
2144 local Exponential = function( c, n ) return c * mp( n, 2.9 ) end
2145 local FindScalar = function( s ) for i = 0, 50, 0.00001 do if Exponential( i, c ) > s then return i end end return 0 end
2146
2147 -- Find scalars for the rank ladders
2148 local OutNormal = FindScalar( self.MultiplierNormal * self.LadderScalar )
2149 local OutBonus = FindScalar( self.MultiplierBonus * self.LadderScalar )
2150 local OutAngled = FindScalar( self.MultiplierAngled * self.LadderScalar )
2151
2152 -- Validate them and set them
2153 if OutNormal > 0 and OutBonus > 0 and OutAngled > 0 then
2154 self.NormalScalar = OutNormal
2155 self.BonusScalar = OutBonus
2156 self.AngledScalar = OutAngled
2157 else
2158 print( "Ranking", "Couldn't calculate ranking scalar. Make sure you have at least ONE entry in your game_map!" )
2159 end
2160
2161 -- Generate additional columns for the rank list
2162 for i = 1, c do
2163 Ranks[ i ][ 3 ] = Exponential( self.NormalScalar, i )
2164 Ranks[ i ][ 4 ] = Exponential( self.BonusScalar, i )
2165 Ranks[ i ][ 5 ] = Exponential( self.AngledScalar, i )
2166 end
2167
2168 -- Continue with loading the top lists when we're doing the first load
2169 if not Timer.IsLoaded then
2170 self:LoadTopLists()
2171 self:LoadNotifyCache()
2172 end
2173end
2174
2175--[[
2176 Description: Loads the full top list of players into a table
2177--]]
2178function Player:LoadTopLists()
2179 -- Get all styles to be ranked
2180 Prepare(
2181 "SELECT DISTINCT(nStyle) FROM game_times ORDER BY nStyle ASC"
2182 )( function( data, varArg, szError )
2183 if Core.Assert( data, "nStyle" ) then
2184 for j = 1, #data do
2185 local style = tonumber( data[ j ]["nStyle"] )
2186
2187 -- Check if bonus
2188 if Core.IsValidBonus( style ) then
2189 style = Styles.Bonus
2190 end
2191
2192 -- Create a blank table for this style
2193 if not TopListCache[ style ] then
2194 TopListCache[ style ] = {}
2195 elseif style == Styles.Bonus then
2196 continue
2197 end
2198
2199 -- Get the top players for the selected style
2200 Prepare(
2201 "SELECT szUID, SUM(nPoints) as nSum FROM game_times WHERE nStyle " .. (style == Styles.Bonus and ">" or "") .. "= {0} GROUP BY szUID ORDER BY nSum DESC LIMIT {1}",
2202 { style, self.TopListLimit },
2203 nil, true
2204 )( function( data, varArg, szError )
2205 if Core.Assert( data, "nSum" ) then
2206 local makeNum = tonumber
2207 for j = 1, #data do
2208 data[ j ]["nSum"] = makeNum( data[ j ]["nSum"] )
2209
2210 -- Insert the entry to the total table
2211 TopListCache[ style ][ j ] = data[ j ]
2212 end
2213 end
2214 end )
2215
2216 -- Check if there is any
2217 if TopListCache[ style ][ 1 ] and TopListCache[ style ][ 1 ].szUID then
2218 Timer.Top[ style ] = TopListCache[ style ][ 1 ].szUID
2219 end
2220 end
2221 end
2222 end )
2223
2224 -- Set a variable to track most WRs
2225 local TopWRTrack = {}
2226 Timer.TopWRPlayer = {}
2227
2228 -- Fetch all #1 WRs for each map on each style
2229 Prepare(
2230 "SELECT * FROM (SELECT * FROM game_times ORDER BY nTime DESC) GROUP BY szMap, nStyle ORDER BY nStyle ASC",
2231 nil, nil, true
2232 )( function( data, varArg, szError )
2233 if Core.Assert( data, "nTime" ) then
2234 local makeNum = tonumber
2235 for j = 1, #data do
2236 local id = data[ j ]["szUID"]
2237 local count = 1
2238
2239 if not WRTopCache[ id ] then
2240 WRTopCache[ id ] = {}
2241 else
2242 count = #WRTopCache[ id ] + 1
2243 end
2244
2245 data[ j ]["szUID"] = nil
2246 data[ j ]["nStyle"] = makeNum( data[ j ]["nStyle"] )
2247 data[ j ]["nTime"] = makeNum( data[ j ]["nTime"] )
2248 data[ j ]["nPoints"] = makeNum( data[ j ]["nPoints"] )
2249 data[ j ]["nDate"] = makeNum( data[ j ]["nDate"] )
2250
2251 local style = data[ j ]["nStyle"]
2252 if not TopWRTrack[ style ] then
2253 TopWRTrack[ style ] = {}
2254 end
2255
2256 TopWRTrack[ style ][ id ] = (TopWRTrack[ style ][ id ] or 0) + 1
2257
2258 -- Insert the entry to the total table
2259 WRTopCache[ id ][ count ] = data[ j ]
2260 end
2261 end
2262 end )
2263
2264 -- Compute the top WR players
2265 for style,data in pairs( TopWRTrack ) do
2266 local topn, topu = 0
2267
2268 -- Loop over all data
2269 for uid,count in pairs( data ) do
2270 if count > topn then
2271 topn = count
2272 topu = uid
2273 end
2274 end
2275
2276 -- Set the top WR player for the style
2277 Timer.TopWRPlayer[ style ] = topu
2278 end
2279
2280 -- Copy over the tracking list for further usage
2281 Timer.TopWRList = TopWRTrack
2282
2283 -- Compute data for total rank
2284 Prepare(
2285 "SELECT AVG(nPoints) AS nPoints FROM game_times"
2286 )( function( data, varArg, szError )
2287 if Core.Assert( data, "nPoints" ) then
2288 Player.AveragePoints = tonumber( data[ 1 ]["nPoints"] ) or 1
2289 end
2290 end )
2291end
2292
2293--[[
2294 Description: Loads the WR beaten notifications
2295--]]
2296function Player:LoadNotifyCache()
2297 -- Determine the maximum age
2298 local nThreshold = os.time() - (3600 * 24 * 14)
2299
2300 -- Delete all old entries
2301 Prepare(
2302 "DELETE FROM game_notifications WHERE nDate < {0}",
2303 { nThreshold }
2304 )
2305
2306 -- Fetch all valid notifications
2307 Prepare(
2308 "SELECT * FROM game_notifications"
2309 )( function( data, varArg, szError )
2310 if Core.Assert( data, "szUID" ) then
2311 for j = 1, #data do
2312 local id = data[ j ]["szUID"]
2313 if not Player.NotifyCache[ id ] then
2314 Player.NotifyCache[ id ] = {}
2315 end
2316
2317 table.insert( Player.NotifyCache[ id ], data[ j ] )
2318 end
2319 end
2320 end )
2321end
2322
2323
2324--[[
2325 Description: Loads the player's rank according to their points
2326 Used by: Rank functions
2327--]]
2328function PLAYER:LoadTime( bReload, bPractice )
2329 -- For practice mode we don't really have to load a time
2330 if self.Practice then
2331 self.Record = 0
2332 self.Leaderboard = 0
2333
2334 self:SetObj( "Record", self.Record, bReload )
2335 self:SetObj( "Position", self.Leaderboard, bReload )
2336
2337 -- Only when it's actually changed
2338 if self.SpecialRank != nil then
2339 self.SpecialRank = nil
2340 self:SetObj( "SpecialRank", self.SpecialRank, bReload )
2341 end
2342
2343 -- Send the record
2344 local ar = NetPrepare( "Timer/Record" )
2345 ar:Double( self.Record )
2346
2347 ar:Bit( true )
2348 ar:UInt( self.Style, 8 )
2349 ar:Bit( not not bPractice )
2350
2351 ar:Bit( false )
2352 ar:Send( self )
2353
2354 return false
2355 end
2356
2357 -- For TAS, we direct it elsewhere
2358 if self.TAS then return self.TAS.LoadRecord( self, self.Style ) end
2359
2360 -- Obtain their position in the ladder for their style
2361 local t, r = Timer.GetPlayerRecord( self )
2362 self.Record = t
2363 self.Leaderboard = r
2364 self:SetObj( "Record", self.Record, bReload )
2365 self:SetObj( "Position", self.Leaderboard, bReload )
2366
2367 -- Send the data for fast GUI drawing
2368 local ar = NetPrepare( "Timer/Record" )
2369 ar:Double( self.Record )
2370
2371 ar:Bit( true )
2372 ar:UInt( self.Style, 8 )
2373 ar:Bit( false )
2374
2375 ar:Bit( false )
2376 ar:Send( self )
2377
2378 -- For the top 3, give them a medal
2379 if r <= 3 then
2380 Player.SetRankMedal( self, r, bReload )
2381 elseif self.SpecialRank then
2382 self.SpecialRank = nil
2383 self:SetObj( "SpecialRank", self.SpecialRank )
2384 end
2385end
2386
2387--[[
2388 Description: Loads the player's rank according to their points
2389 Used by: Rank functions
2390--]]
2391function PLAYER:LoadRank( bJoin, bReload )
2392 local nStyle = self.Style
2393
2394 -- When on Practice, we reset all data
2395 if self.Practice then
2396 self.Rank = -10
2397 self:SetObj( "Rank", self.Rank, bReload )
2398
2399 self.SubRank = 0
2400 self:SetObj( "SubRank", self.SubRank, bReload )
2401
2402 self.CurrentPointSum = 0
2403 self.CurrentMapSum = 0
2404 else
2405 -- For TAS, we direct it elsewhere
2406 if self.TAS then return self.TAS.LoadRank( self ) end
2407
2408 -- Obtain the data from the cache and database
2409 Player.GetPointSum( self, nStyle, bJoin, function( ply, Points, MapPoints )
2410 -- Only update if the whole rank is actually different
2411 local Rank = Player.GetRank( Points, Player.GetRankType( nStyle ) )
2412 if Rank != ply.Rank then
2413 ply.Rank = Rank
2414 ply:SetObj( "Rank", ply.Rank, bReload )
2415 end
2416
2417 -- Set the current values for later usage
2418 ply.CurrentPointSum = Points
2419 ply.CurrentMapSum = MapPoints
2420
2421 -- Set their sub rank
2422 Player.SetSubRank( ply, Rank, Points, bReload )
2423 end )
2424 end
2425
2426 -- On the first join, give them the scalars
2427 if bJoin then
2428 local ar = NetPrepare( "Timer/Ranks" )
2429 ar:Double( Player.NormalScalar )
2430 ar:Double( Player.BonusScalar )
2431 ar:Double( Player.AngledScalar )
2432 ar:Send( self )
2433 end
2434end
2435
2436--[[
2437 Description: Loads the player into a new style and sets the appropriate values
2438 Used by: Commands
2439--]]
2440function PLAYER:LoadStyle( nStyle, nBonus )
2441 -- Validate the style again
2442 if not nStyle or (nStyle < Styles.Normal and not Config.Modes[ nStyle ]) or nStyle > Config.MaxStyle then return end
2443
2444 -- If we're setting a bonus style, add the bonus ID to the style
2445 if nBonus then
2446 nStyle = nStyle + nBonus
2447 end
2448
2449 -- Set the style variables
2450 local OldStyle, PreviousStyle, NextPractice = self.Style, self.Style
2451 if nStyle == Config.PracticeStyle then
2452 if not self.Practice then
2453 -- Clean the bot, of course
2454 self:CleanFrames()
2455 self:SetBotActive( nil )
2456
2457 -- Set the style to the style we were on at first
2458 self.Style = OldStyle
2459 self:SetObj( "Style", self.Style )
2460
2461 -- Enable practice mode on server and client
2462 self.Practice = true
2463 OldStyle = true
2464 else
2465 -- Set the style to the style we were on
2466 self.Style = OldStyle
2467 self:SetObj( "Style", self.Style )
2468
2469 -- Disable the practice mode and update client
2470 self.Practice = nil
2471 OldStyle = nil
2472 NextPractice = true
2473 end
2474 else
2475 if self.Practice then
2476 if not Core.IsValidBonus( nStyle ) then
2477 -- Send a message about practice mode
2478 Core.Print( self, "Timer", Core.Text( "StylePracticeEnabled" ) )
2479 end
2480
2481 -- Make sure we're good with the styles
2482 self.Style = nStyle
2483 self:SetObj( "Style", self.Style )
2484
2485 -- Update on the client (just a double measure)
2486 OldStyle = true
2487 else
2488 -- Set the styles
2489 self.Style = nStyle
2490 self:SetObj( "Style", self.Style )
2491
2492 -- Make sure we change style normally
2493 OldStyle = nil
2494 end
2495 end
2496
2497 if not OldStyle then
2498 -- Reset without copying function addresses
2499 concommand.Run( self, "reset", "bypass", "" )
2500 end
2501
2502 -- Reset attributes
2503 ResetPlayerAttributes( self, PreviousStyle )
2504
2505 -- Now loads the actual values in
2506 self:LoadTime( nil, OldStyle )
2507 self:LoadRank()
2508
2509 -- Publish all variable changes
2510 self:PublishObj()
2511
2512 -- Let them know what happened
2513 local PracticeText = ""
2514 if OldStyle == true then
2515 PracticeText = " (With practice mode enabled)"
2516 elseif NextPractice then
2517 PracticeText = " (Disabled practice mode)"
2518 end
2519
2520 Core.Print( self, "Timer", Core.Text( "StyleChange", Core.StyleName( self.Style ), PracticeText ) )
2521end
2522
2523
2524--[[
2525 Description: Gets the amount of points you have in a specific style
2526 Used by: Rank functions
2527--]]
2528function Player.GetPointSum( ply, nStyle, bJoin, fCall )
2529 -- Forces style to bonus
2530 if Core.IsValidBonus( nStyle ) then
2531 nStyle = Styles.Bonus
2532 end
2533
2534 -- Fetch the data
2535 if (not StylePoints[ nStyle ] or not StylePoints[ nStyle ][ ply.UID ]) and not bJoin then
2536 Prepare(
2537 "SELECT SUM(nPoints) AS nSum FROM game_times WHERE szUID = {0} AND nStyle " .. (nStyle == Styles.Bonus and ">" or "") .. "= {1} AND szMap != {2}",
2538 { ply.UID, nStyle, Timer:GetMap() }
2539 )( function( data, varArg, szError )
2540 local OtherPoints = 0
2541 if Core.Assert( data, "nSum" ) then
2542 OtherPoints = tonumber( data[ 1 ]["nSum"] ) or 0
2543
2544 if StylePoints[ nStyle ] then
2545 StylePoints[ nStyle ][ ply.UID ] = OtherPoints
2546 end
2547 end
2548
2549 local MapPoints = Timer.GetPointsForMap( ply, ply.Record, nStyle )
2550 fCall( ply, OtherPoints + MapPoints, MapPoints )
2551 end )
2552 else
2553 local MapPoints = Timer.GetPointsForMap( ply, ply.Record, nStyle )
2554 fCall( ply, (StylePoints[ nStyle ][ ply.UID ] or 0) + MapPoints, MapPoints )
2555 end
2556end
2557
2558--[[
2559 Description: Gets your rank using a given amount of points against a certain ladder type
2560 Used by: Rank functions
2561--]]
2562function Player.GetRank( nPoints, nType )
2563 local Rank = 1
2564
2565 for i = 1, #Ranks do
2566 if i > Rank and nPoints >= Ranks[ i ][ nType ] then
2567 Rank = i
2568 end
2569 end
2570
2571 return Rank
2572end
2573
2574--[[
2575 Description: Obtains the type of rank ladder for the style
2576 Used by: Rank functions
2577--]]
2578function Player.GetRankType( nStyle )
2579 return Config.RankColumns[ nStyle ] or 3
2580end
2581
2582--[[
2583 Description: Reloads the rank and sub rank on all relevant players
2584 Used by: Time adding system
2585--]]
2586function Player.SetSubRank( ply, nRank, nPoints, bReload )
2587 -- Check if the player is the one with most WRs
2588 local nTarget
2589 if Timer.TopWRPlayer[ ply.Style ] == ply.UID then
2590 nTarget = ply.Style == Styles.Normal and 13 or 11
2591 end
2592
2593 -- Checks if you're top rank or not
2594 if nRank >= #Ranks or nTarget then
2595 if not nTarget then
2596 -- Set the default value
2597 nTarget = 10
2598
2599 -- Check if they are at the top
2600 local style = Core.IsValidBonus( ply.Style ) and Styles.Bonus or ply.Style
2601 if Timer.Top[ style ] == ply.UID then
2602 nTarget = 12
2603 end
2604 end
2605
2606 -- Sets it to the custom rank icons
2607 if ply.SubRank != nTarget then
2608 ply.SubRank = nTarget
2609 ply:SetObj( "SubRank", ply.SubRank, bReload )
2610 end
2611 else
2612 -- Get the column id
2613 local ColID = Player.GetRankType( ply.Style )
2614
2615 -- Calculate the step size over 10 steps in between
2616 local StepSize = (Ranks[ nRank + 1 ][ ColID ] - Ranks[ nRank ][ ColID ]) / 10
2617
2618 -- Iterate over all steps and see in which they fall
2619 local nOut, nStep = 1, 1
2620 for i = Ranks[ nRank ][ ColID ], Ranks[ nRank + 1 ][ ColID ], StepSize do
2621 if nPoints >= i then
2622 nOut = nStep
2623 end
2624
2625 nStep = nStep + 1
2626 end
2627
2628 -- Only change sub rank if it's different
2629 if ply.SubRank != nOut then
2630 ply.SubRank = nOut
2631 ply:SetObj( "SubRank", ply.SubRank, bReload )
2632 end
2633 end
2634end
2635
2636--[[
2637 Description: Reloads the rank and sub rank on all relevant players
2638 Used by: Time adding system
2639--]]
2640function Player.ReloadRanks( sender, nStyle, nOldAverage )
2641 -- Get the multiplier for the given style
2642 local nMultiplier = Timer:GetMultiplier( nStyle )
2643 if nMultiplier == 0 then return end
2644
2645 -- Create a new table for changed players
2646 local plys = {}
2647
2648 -- Get the new average
2649 local nAverage = GetAverage( nStyle )
2650 for _,p in pairs( player.GetHumans() ) do
2651 -- Only reload for relevant players
2652 if p == sender or p.Style != nStyle or p.Record == 0 or not p.CurrentPointSum then continue end
2653
2654 local CurrentPoints = Timer.GetPointsForMap( p, p.Record, p.Style, nOldAverage )
2655 local NewPoints = Timer.GetPointsForMap( p, p.Record, p.Style, nAverage )
2656 local Points = p.CurrentPointSum - CurrentPoints + NewPoints
2657
2658 local Rank = Player.GetRank( Points, Player.GetRankType( p.Style ) )
2659 if Rank != p.Rank then
2660 p.Rank = Rank
2661 p:SetObj( "Rank", p.Rank )
2662 end
2663
2664 -- Set the new sum for future reloads
2665 p.CurrentPointSum = Points
2666
2667 -- Also reload their sub rank
2668 Player.SetSubRank( p, p.Rank, p.CurrentPointSum )
2669
2670 -- Get their new leaderboard id
2671 local t, r = Timer.GetPlayerRecord( p )
2672 if r != p.Leaderboard then
2673 p.Leaderboard = r
2674 p:SetObj( "Position", p.Leaderboard )
2675 end
2676
2677 -- Add this player to the broadcast list
2678 plys[ #plys + 1 ] = p
2679 end
2680
2681 -- Commit the changes
2682 Core.PublishPlayers( plys )
2683end
2684
2685--[[
2686 Description: Sets the player's medal
2687 Used by: New record obtaining, joining functions
2688--]]
2689function Player.SetRankMedal( ply, nPos, bReload )
2690 if not bReload then
2691 ply.SpecialRank = nPos
2692 ply:SetObj( "SpecialRank", ply.SpecialRank )
2693 else
2694 local nStyle = ply.Style
2695 local list = Timer.GetTopSteam( nStyle, 3 ) -- Gets the top 3 steam ids
2696
2697 local function HasValue( tab, v )
2698 for i = 1, #tab do
2699 if tab[ i ] == v then
2700 return i
2701 end
2702 end
2703 end
2704
2705 for _,p in pairs( player.GetHumans() ) do
2706 if p.Style != nStyle then continue end
2707 local AtID = HasValue( list, p.UID )
2708 if AtID then
2709 p.SpecialRank = AtID
2710 p:SetObj( "SpecialRank", p.SpecialRank, true )
2711 elseif p.SpecialRank then
2712 p.SpecialRank = nil
2713 p:SetObj( "SpecialRank", p.SpecialRank, true )
2714 end
2715 end
2716 end
2717end
2718
2719--[[
2720 Description: Gets the details of a player and returns it to the requesting player
2721--]]
2722function Player.ReceiveScoreboard( ply, varArgs )
2723 local id, target = varArgs[ 1 ]
2724 for _,p in pairs( player.GetHumans() ) do
2725 if p.UID == id then
2726 target = p
2727 break
2728 end
2729 end
2730
2731 if IsValid( target ) then
2732 local tab = { WRs = Timer.GetPlayerWRs( target.UID, target.Style ) }
2733 tab.Target = target.UID
2734 tab.Online = target.ConnectedAt and ST() - target.ConnectedAt or 0
2735 tab.Timer = (target.Tb and not target.TbF) and ST() - target.Tb or ((target.Tn and not target.TnF) and ST() - target.Tn or -1) or -1
2736 tab.Stage = not target.Practice and target.TnId
2737 tab.TAS = target.TAS and (target.TAS.GetTimer( target ) or 0)
2738
2739 -- Fetch the average points
2740 if not Player.AveragePointsCache[ target ] then
2741 Prepare(
2742 "SELECT AVG(nPoints) AS nPoints FROM game_times WHERE szUID = {0}",
2743 { target:SteamID() }
2744 )( function( data, varArg, szError )
2745 if Core.Assert( data, "nPoints" ) then
2746 Player.AveragePointsCache[ target ] = tonumber( data[ 1 ]["nPoints"] ) or 0
2747 else
2748 Player.AveragePointsCache[ target ] = 0
2749 end
2750 end )
2751 end
2752
2753 tab.TotalRank = math.Round( (Player.AveragePointsCache[ target ] / Player.AveragePoints) * 100.0, 1 )
2754 tab.MapPoints = { math.Round( Timer.GetPointsForMap( target, target.Record, target.Style ), 2 ), Timer:GetMultiplier( target.Style ) }
2755 tab.MapsBeat = Core.HandlePlayerMaps( "Beat", target, { GetCount = true } ) or 0
2756
2757 Core.Send( ply, "GUI/Scoreboard", tab )
2758 end
2759end
2760Core.Register( "Global/Scoreboard", Player.ReceiveScoreboard )
2761
2762--[[
2763 Description: Gets geographic location of the IP
2764 Used: Player profiles
2765--]]
2766function Player.GetGeoLocation( ply, ip, callback )
2767 if not ip then
2768 return callback()
2769 end
2770
2771 if not Player.GeoLocations then
2772 Player.GeoLocations = {}
2773 end
2774
2775 if not Player.GeoLocations[ ip ] then
2776 Core.Print( ply, "General", Core.Text( "CommandProfileFetching" ) )
2777
2778 http.Fetch(
2779 "http://www.geoplugin.net/json.gp?ip=" .. ip,
2780 function( body )
2781 local json = util.JSONToTable( body ) or {}
2782 if json.geoplugin_countryCode and json.geoplugin_countryName and json.geoplugin_countryCode != "" then
2783 Player.GeoLocations[ json.geoplugin_request ] = { Code = json.geoplugin_countryCode, Name = json.geoplugin_countryName }
2784 return callback( Player.GeoLocations[ json.geoplugin_request ] )
2785 end
2786
2787 callback()
2788 end,
2789 function()
2790 callback()
2791 end
2792 )
2793 else
2794 callback( Player.GeoLocations[ ip ] )
2795 end
2796end
2797
2798--[[
2799 Description: Shows the target player information on the given player
2800--]]
2801function Player.ShowProfile( ply, steam, ip )
2802 if ply.FetchingProfile then
2803 return Core.Print( ply, "General", Core.Text( "CommandProfileBusy" ) )
2804 end
2805
2806 ply.FetchingProfile = true
2807
2808 Player.GetGeoLocation( ply, ip, function( loc )
2809 local tab = {}
2810 tab.Steam = steam
2811 tab.Location = loc and loc.Name
2812
2813 local wrs = Timer.GetPlayerWRs( steam, nil, true )
2814 local sortable = {}
2815
2816 for style,count in pairs( wrs.Rest or {} ) do
2817 sortable[ #sortable + 1 ] = { Style = style, Count = count }
2818 end
2819
2820 tab.WRs = wrs[ 1 ]
2821 table.SortByMember( sortable, "Count" )
2822
2823 if #sortable > 0 then
2824 tab.PrimeWR = {}
2825
2826 for i = 1, #sortable do
2827 tab.PrimeWR[ i ] = { Core.StyleName( sortable[ i ].Style ), sortable[ i ].Count }
2828 end
2829 end
2830
2831 tab.Points = {}
2832 tab.TopPoints = {}
2833 tab.PlayerPos = {}
2834 tab.Players = Timer.PlayerCount or {}
2835
2836 Prepare(
2837 "SELECT nStyle, SUM(nPoints) AS nSum FROM game_times WHERE szUID = {0} GROUP BY nStyle",
2838 { steam }
2839 )( function( data, varArg, szError )
2840 if Core.Assert( data, "nSum" ) then
2841 for j = 1, #data do
2842 local style = tonumber( data[ j ]["nStyle"] )
2843 local points = tonumber( data[ j ]["nSum"] ) or 0
2844
2845 if Core.IsValidBonus( style ) then
2846 tab.Points[ Styles.Bonus ] = (tab.Points[ Styles.Bonus ] or 0) + points
2847 else
2848 tab.Points[ style ] = points
2849 end
2850 end
2851 end
2852 end )
2853
2854 for style,data in pairs( Timer.PlayerLadderPos ) do
2855 tab.PlayerPos[ style ] = data[ steam ] or 0
2856 end
2857
2858 for style,data in pairs( TopListCache ) do
2859 if data[ 1 ] and data[ 1 ]["nSum"] then
2860 tab.TopPoints[ style ] = data[ 1 ]["nSum"]
2861 end
2862 end
2863
2864 tab.MapsBeat = Core.HandlePlayerMaps( "Beat", { UID = steam, Style = 1 }, { "Filler", Upper = { steam }, GetCount = true } ) or 0
2865 tab.MapsTotal = Timer.Maps
2866
2867 Prepare(
2868 "SELECT COUNT(szMap) AS nCount FROM game_stagetimes WHERE szUID = {0} AND nStyle = 1",
2869 { steam }
2870 )( function( data, varArg, szError )
2871 if Core.Assert( data, "nCount" ) then
2872 tab.CPRs = tonumber( data[ 1 ]["nCount"] ) or 0
2873 end
2874 end )
2875
2876 Prepare(
2877 "SELECT szMap, nStyle, nTime, nDate FROM game_times WHERE szUID = {0} ORDER BY nDate DESC LIMIT 10",
2878 { steam }
2879 )( function( data, varArg, szError )
2880 if Core.Assert( data, "nDate" ) then
2881 tab.Recent = data
2882 end
2883 end )
2884
2885 Core.Send( ply, "GUI/Create", { ID = "Profile", Dimension = { x = 200, y = 100, px = 20 }, Args = { Title = "Player Profile", Custom = tab } } )
2886 ply.FetchingProfile = nil
2887 end )
2888end
2889Core.ShowProfile = Player.ShowProfile
2890
2891--[[
2892 Description: Handles a player's full connection
2893--]]
2894function Player.ReceiveEntry( ply, varArgs )
2895 -- Make sure the player also receives all player data
2896 ply:InitialObj()
2897
2898 -- Sending the platforms
2899 if varArgs.Platforms then
2900 Zones.SendPlatforms( ply )
2901 end
2902
2903 -- Check if we want the simple HUD
2904 if varArgs.Simple then
2905 Core.Trigger( "Global/Simple", { true }, nil, ply )
2906 end
2907
2908 -- Check if we have perma sync on
2909 if varArgs.Sync then
2910 Core.Trigger( "Global/PermSync", { true }, nil, ply )
2911 end
2912
2913 -- Check if we want a model
2914 if varArgs.Model then
2915 Core.Trigger( "Global/Model", { varArgs.Model, true }, nil, ply )
2916 end
2917
2918 -- Check if we want the simple HUD
2919 if varArgs.Third then
2920 Core.Trigger( "Global/Thirdperson", { true }, nil, ply )
2921 end
2922
2923 -- Check if we have the time kicker on
2924 if varArgs.Kick then
2925 local func = Core.GetCmd( "remainingtries" )
2926 func( ply, { "time", varArgs.Kick, Key = "remainingtries" } )
2927 end
2928
2929 -- Check if there's a custom style to be applied
2930 if ply.CustomStyleFunc then
2931 ply:CustomStyleFunc()
2932 elseif varArgs.Style then
2933 if tonumber( varArgs.Style ) and ply.Style != varArgs.Style then
2934 concommand.Run( ply, "style", tostring( varArgs.Style ), "" )
2935 end
2936 end
2937
2938 -- Check if something went wrong with the RTV system
2939 if ST() > RTV.End and (timer.TimeLeft( RTV.Identifier ) or 0) > 30 * 60 and not RTV.ResetBreak then
2940 RTV.ResetBreak = true
2941 RTV:ResetVote( "Yes", 1, false, "VoteFailure" )
2942 end
2943end
2944Core.Register( "Global/Entry", Player.ReceiveEntry )
2945
2946--[[
2947 Description: Prints any type of message as a replacement to the regular notifications that used to be in place
2948--]]
2949function Player.Notification( ply, szType, details )
2950 local colors = Config.Colors
2951
2952 if szType == "BaseFinish" then
2953 local viewers = ply:Spectator( "Get", { true } )
2954 local szMessage = Core.ColorText()
2955 local szMessageRemote = Core.ColorText()
2956
2957 if Core.IsValidBonus( ply.Style ) then
2958 szMessage:Add( "You finished bonus [" )
2959 szMessage:Add( Core.StyleName( ply.Style ), colors[ 1 ], true )
2960 szMessage:Add( "] in " )
2961 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
2962
2963 if details.Difference != "" then
2964 szMessage:Add( " (" )
2965 szMessage:Add( details.Difference, colors[ 1 ], true )
2966 szMessage:Add( ")" )
2967 end
2968
2969 if #viewers > 0 then
2970 szMessageRemote:Copy( szMessage )
2971 szMessageRemote:Replace( 1, 4, ply:Name(), colors[ 1 ], true )
2972 szMessageRemote:Add( " (" .. details.Jumps .. " jumps, " .. details.Strafes .. " strafes with " .. details.Sync .. "% sync)" )
2973 end
2974 else
2975 szMessage:Add( "You finished" )
2976
2977 if ply.Style > Styles.Normal then
2978 szMessage:Add( " " .. Core.StyleName( ply.Style ), colors[ 4 ], true )
2979 end
2980
2981 szMessage:Add( " in " )
2982 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
2983
2984 if details.Difference != "" then
2985 szMessage:Add( " (" )
2986 szMessage:Add( details.Difference, colors[ 1 ], true )
2987 szMessage:Add( ")" )
2988 end
2989
2990 if #viewers > 0 then
2991 szMessageRemote:Copy( szMessage )
2992 szMessageRemote:Replace( 1, 4, ply:Name(), colors[ 1 ], true )
2993 szMessageRemote:Add( " (" .. details.Jumps .. " jumps, " .. details.Strafes .. " strafes with " .. details.Sync .. "% sync)" )
2994 end
2995 end
2996
2997 local ar = NetPrepare( "Timer/Finish" )
2998 ar:Double( details.Time )
2999 ar:UInt( details.Jumps, 16 )
3000 ar:UInt( details.Strafes, 16 )
3001 ar:Double( details.Sync )
3002
3003 if details.Points then
3004 ar:Bit( true )
3005 ar:Double( details.Points )
3006 else
3007 ar:Bit( false )
3008 end
3009
3010 ar:ColorText( szMessage:Get() )
3011 ar:Send( ply )
3012
3013 if #viewers > 0 and szMessageRemote:Count() > 0 then
3014 ar = NetPrepare( "NotifyMulti" )
3015 ar:String( szType )
3016 ar:ColorText( szMessageRemote:Get() )
3017 ar:Send( viewers )
3018 end
3019 elseif szType == "ImproveFinish" then
3020 local szMessage = Core.ColorText()
3021 local szMessageTop = Core.ColorText()
3022
3023 if Core.IsValidBonus( details.Style ) then
3024 szMessage:Add( ply:Name(), colors[ 1 ], true )
3025 szMessage:Add( " finished bonus [" )
3026 szMessage:Add( Core.StyleName( details.Style ), colors[ 1 ], true )
3027 szMessage:Add( "] in " )
3028 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
3029
3030 if details.DifferenceWR != "" then
3031 szMessage:Add( " (" )
3032 szMessage:Add( details.DifferenceWR, colors[ 1 ], true )
3033
3034 if details.Improvement != -1 then
3035 szMessage:Add( ", " )
3036 szMessage:Add( "Improved by " .. details.Improvement, colors[ 3 ], true )
3037 end
3038
3039 szMessage:Add( ")" )
3040 end
3041
3042 szMessage:Add( " [Rank " .. details.Rank .. "]" )
3043
3044 if details.MapRecord then
3045 szMessageTop:Add( ply:Name(), colors[ 1 ], true )
3046 szMessageTop:Add( " took the #1 place in the " )
3047 szMessageTop:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3048 szMessageTop:Add( " leaderboards!" )
3049
3050 if details.Bot then
3051 szMessageTop:Add( " The bot is now displaying this run!" )
3052 end
3053 else
3054 local space = ""
3055
3056 if details.Bot then
3057 szMessageTop:Add( space .. "The bot is now displaying this run since it is the fastest run available!" )
3058 end
3059 end
3060
3061 -- To make sure we don't have Bonus 2 showing up with Bonus disabled
3062 if details.Style != Styles.Bonus then
3063 details.Style = Styles.Bonus
3064 end
3065 else
3066 szMessage:Add( ply:Name(), colors[ 1 ], true )
3067 szMessage:Add( " finished " )
3068
3069 if details.Style > Styles.Normal then
3070 szMessage:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3071 szMessage:Add( " in " )
3072 else
3073 szMessage:Add( "in " )
3074 end
3075
3076 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
3077
3078 if details.DifferenceWR != "" then
3079 szMessage:Add( " (" )
3080 szMessage:Add( details.DifferenceWR, colors[ 1 ], true )
3081
3082 if details.Improvement != -1 then
3083 szMessage:Add( ", " )
3084 szMessage:Add( "Improved by " .. details.Improvement, colors[ 3 ], true )
3085 end
3086
3087 szMessage:Add( ")" )
3088 end
3089
3090 szMessage:Add( " [Rank " .. details.Rank .. "]" )
3091
3092 if details.MapRecord then
3093 szMessageTop:Add( ply:Name(), colors[ 1 ], true )
3094
3095 if details.Style > Styles.Normal then
3096 szMessageTop:Add( " took the #1 place in the " )
3097 szMessageTop:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3098 szMessageTop:Add( " leaderboards!" )
3099 else
3100 szMessageTop:Add( " took the #1 place in the Normal leaderboards!" )
3101 end
3102
3103 if details.Bot then
3104 szMessageTop:Add( " The bot is now displaying this run!" )
3105 end
3106 else
3107 local space = ""
3108
3109 if details.Bot then
3110 szMessageTop:Add( space .. "The bot is now displaying this run since it is the fastest run available!" )
3111 end
3112 end
3113 end
3114
3115 local ar = NetPrepare( "NotifyMulti" )
3116 ar:String( szType )
3117 ar:ColorText( szMessage:Get() )
3118 ar:UInt( details.Pos, 16 )
3119 ar:UInt( details.Style, 8 )
3120 ar:UInt( ply:EntIndex(), 16 )
3121
3122 if details.Sound then
3123 ar:Bit( true )
3124 ar:String( details.Sound )
3125 else
3126 ar:Bit( false )
3127 end
3128
3129 if szMessageTop:Count() > 0 then
3130 ar:Bit( true )
3131 ar:ColorText( szMessageTop:Get() )
3132 else
3133 ar:Bit( false )
3134 end
3135
3136 ar:Broadcast()
3137 elseif szType == "StageSlow" then
3138 local viewers = ply:Spectator( "Get", { true } )
3139 local szText = details.Linear and "Checkpoint " or "Stage "
3140 local szMessage = Core.ColorText()
3141 local szMessageRemote = Core.ColorText()
3142
3143 szMessage:Add( "You finished [" )
3144 szMessage:Add( szText .. details.ID, colors[ 1 ], true )
3145 szMessage:Add( "]" )
3146
3147 if details.Style > Styles.Normal then
3148 szMessage:Add( " on " )
3149 szMessage:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3150 end
3151
3152 szMessage:Add( " in " )
3153 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
3154
3155 if details.DifferencePB != "" then
3156 szMessage:Add( " (" )
3157 szMessage:Add( details.DifferencePB, colors[ 3 ], true )
3158
3159 if details.DifferenceWR != "" then
3160 szMessage:Add( ", " )
3161 szMessage:Add( details.DifferenceWR, colors[ 1 ], true )
3162 end
3163
3164 szMessage:Add( ")" )
3165 end
3166
3167 if #viewers > 0 then
3168 szMessageRemote:Copy( szMessage )
3169 szMessageRemote:Replace( 1, 4, ply:Name(), colors[ 1 ], true )
3170 end
3171
3172 local ar = NetPrepare( "NotifyMulti" )
3173 ar:String( szType )
3174 ar:ColorText( szMessage:Get() )
3175 ar:Bit( false )
3176 ar:Send( ply )
3177
3178 if #viewers > 0 and szMessageRemote:Count() > 0 then
3179 ar = NetPrepare( "NotifyMulti" )
3180 ar:String( szType )
3181 ar:ColorText( szMessageRemote:Get() )
3182 ar:Bit( true )
3183 ar:Send( viewers )
3184 end
3185 elseif szType == "StageFast" then
3186 local szText = details.Linear and "Checkpoint " or "Stage "
3187 local szMessage = Core.ColorText()
3188 local szMessageTop = Core.ColorText()
3189 local szMessageRemote = Core.ColorText()
3190
3191 szMessage:Add( "You finished [" )
3192 szMessage:Add( szText .. details.ID, colors[ 1 ], true )
3193 szMessage:Add( "]" )
3194
3195 if details.Style > Styles.Normal then
3196 szMessage:Add( " on " )
3197 szMessage:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3198 end
3199
3200 szMessage:Add( " in " )
3201 szMessage:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
3202
3203 if details.DifferenceWR != "" then
3204 szMessage:Add( " (" )
3205 szMessage:Add( details.DifferenceWR, colors[ 1 ], true )
3206
3207 if details.DifferencePB != "" then
3208 szMessage:Add( ", " )
3209 szMessage:Add( details.DifferencePB, colors[ 3 ], true )
3210 end
3211
3212 szMessage:Add( ")" )
3213 end
3214
3215 szMessage:Add( " [Rank " .. details.Rank .. "]" )
3216
3217 szMessageRemote:Copy( szMessage )
3218 szMessageRemote:Replace( 1, 4, ply:Name(), colors[ 1 ], true )
3219
3220 if details.Pos == 1 then
3221 szMessageTop:Add( ply:Name(), colors[ 1 ], true )
3222
3223 if details.Style > Styles.Normal then
3224 szMessageTop:Add( " took the " )
3225 szMessageTop:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3226 szMessageTop:Add( " record for [" )
3227 else
3228 szMessageTop:Add( " took the record for [" )
3229 end
3230
3231 szMessageTop:Add( szText .. details.ID, colors[ 1 ], true )
3232 szMessageTop:Add( "]" )
3233
3234 if details.Bot then
3235 szMessageTop:Add( "\nThe bot can now display this run!" )
3236 end
3237 end
3238
3239 local ar = NetPrepare( "NotifyMulti" )
3240 ar:String( szType )
3241 ar:ColorText( szMessage:Get() )
3242 ar:ColorText( szMessageRemote:Get() )
3243
3244 if szMessageTop:Count() > 0 then
3245 ar:Bit( true )
3246 ar:ColorText( szMessageTop:Get() )
3247 else
3248 ar:Bit( false )
3249 end
3250
3251 ar:UInt( details.Pos, 16 )
3252 ar:UInt( details.Style, 8 )
3253 ar:UInt( ply:EntIndex(), 16 )
3254
3255 ar:Broadcast()
3256 elseif szType == "TAS" then
3257 Core.Print( ply, "Timer", Core.Text( "TASTimerWR", Core.StyleName( details.Style ) ) )
3258
3259 local szMessageTop = Core.ColorText()
3260 szMessageTop:Add( "[TAS] " )
3261 szMessageTop:Add( ply:Name(), colors[ 1 ], true )
3262 szMessageTop:Add( " made a new " )
3263 szMessageTop:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3264 szMessageTop:Add( " run, with a time of " )
3265 szMessageTop:Add( Timer.Convert( details.Time ), colors[ 2 ], true )
3266
3267 local ar = NetPrepare( "NotifyMulti" )
3268 ar:String( szType )
3269 ar:ColorText( szMessageTop:Get() )
3270 ar:Broadcast()
3271 elseif szType == "Popup" then
3272 if Popups[ ply ] and ST() - Popups[ ply ] < 1 then return end
3273
3274 NetPrepare( "Notify", details ):Send( ply )
3275 Popups[ ply ] = ST()
3276
3277 local viewers = ply:Spectator( "Get", { true } )
3278 if #viewers > 0 then
3279 local ar = NetPrepare( "NotifyMulti" )
3280 ar:String( szType )
3281 ar:Pattern( "Notify", details )
3282 ar:Send( viewers )
3283 end
3284 elseif szType == "LJ" then
3285 local szMessage = Core.ColorText()
3286
3287 szMessage:Add( details.Player, colors[ 1 ], true )
3288 szMessage:Add( " got a " )
3289 szMessage:Add( details.Distance .. " unit", colors[ 2 ], true )
3290 szMessage:Add( " LJ" )
3291
3292 if details.Style > Styles.Normal then
3293 szMessage:Add( " on " )
3294 szMessage:Add( Core.StyleName( details.Style ), colors[ 4 ], true )
3295 end
3296
3297 szMessage:Add( "!" )
3298
3299 if details.Position then
3300 szMessage:Add( " A new personal best, bringing them to #" .. details.Position .. " in the LJ top list!" )
3301 end
3302
3303 local ar = NetPrepare( "NotifyMulti" )
3304 ar:String( szType )
3305 ar:String( details.Player )
3306 ar:Double( details.Distance )
3307 ar:Double( details.Prestrafe )
3308 ar:Double( details.Sync )
3309 ar:UInt( details.Count, 8 )
3310
3311 if details.Edge and details.Duck then
3312 ar:Bit( true )
3313 ar:Bit( details.Duck )
3314 ar:Double( details.Edge )
3315 else
3316 ar:Bit( false )
3317 end
3318
3319 ar:ColorText( szMessage:Get() )
3320 ar:Broadcast()
3321 end
3322end
3323Core.PlayerNotification = Player.Notification
3324
3325--[[
3326 Description: Lets the player know about any possibly beaten times
3327--]]
3328function Player.NotifyBeatenWR( szPreviousWR, szMap, szName, nStyle, nDifference )
3329 -- Check if the previous WR holder is online
3330 local bOnline = player.GetBySteamID( szPreviousWR )
3331
3332 -- If this isn't the case, save it to our table
3333 if not bOnline then
3334 Prepare(
3335 "INSERT INTO game_notifications (szUID, szMap, szName, nStyle, nDifference, nDate) VALUES ({0}, {1}, {2}, {3}, {4}, {5})",
3336 { szPreviousWR, szMap, szName, nStyle, nDifference, os.time() }
3337 )
3338 end
3339end
3340
3341--[[
3342 Description: Lets the player know about any possibly beaten times
3343 Used by: Player initial spawn
3344--]]
3345function PLAYER:NotifyBeatenTimes()
3346 local data = Player.NotifyCache[ self.UID ]
3347 if data then
3348 -- Build the messages
3349 local msg = {}
3350 for j = 1, #data do
3351 msg[ #msg + 1 ] = "- [" .. os.date( "%Y-%m-%d", data[ j ]["nDate"] ) .. "] " .. data[ j ]["szMap"] .. " on " .. Core.StyleName( data[ j ]["nStyle"] ) .. " by " .. data[ j ]["szName"] .. " (-" .. Timer.Convert( data[ j ]["nDifference"] ) .. ")"
3352 end
3353
3354 -- Send it to the player in the proper format
3355 if #msg > 0 then
3356 NetPrepare( "Notify", { "General", Core.Text( "PlayerBeatenPopup", #msg ), "time_delete", 8, #msg < 20 and Core.Text( "PlayerBeatenTime", self:Name(), string.Implode( "\n", msg ), data[ 1 ]["szMap"] ) } ):Send( self )
3357 end
3358
3359 -- Clear the table to avoid seeing this again after a rejoin
3360 Player.NotifyCache[ self.UID ] = {}
3361
3362 -- Get rid of the items in the database
3363 Prepare(
3364 "DELETE FROM game_notifications WHERE szUID = {0}",
3365 { self.UID }
3366 )
3367 end
3368end
3369
3370
3371-- Connections and player handling
3372
3373--[[
3374 Description: Called by the AFK kicker addon if available on the player
3375--]]
3376function PLAYER:AFKFunc( AFK )
3377 local nPoints = AFK.Points[ self ]
3378 if nPoints == 1 then
3379 Core.Print( self, "Timer", Core.Text( "MiscAFK", math.floor( ( (AFK.StartPoints - nPoints) * AFK.CheckInterval) / 60 ) ) )
3380
3381 self.AFKTab = AFK
3382
3383 if not self.Spectating then
3384 concommand.Run( self, "spectate", "bypass", "" )
3385 end
3386 elseif nPoints == 0 then
3387 if #player.GetHumans() >= Config.KickTime then
3388 Core.Print( self, "Timer", Core.Text( "MiscAFKKicked", math.floor( ( (AFK.StartPoints - nPoints) * AFK.CheckInterval) / 60 ) ) )
3389 else
3390 AFK.Points[ self ] = nPoints + 1
3391
3392 local _x, _o, _a = self:IsPlayerDequeued()
3393 if _a then
3394 self:CleanFrames()
3395 self:SetBotActive( nil )
3396 end
3397
3398 return true
3399 end
3400 end
3401
3402 return false
3403end
3404
3405--[[
3406 Description: Player disconnection hook to clean up the trash they made
3407--]]
3408local function PlayerDisconnect( ply )
3409 -- Bots don't need any other logic
3410 if ply:IsBot() then return end
3411
3412 -- When we're all empty, unload the gamemode (save bots)
3413 if #player.GetHumans() - 1 < 1 then
3414 GAMEMODE:UnloadGamemode( "Change" )
3415 end
3416
3417 -- Notify spectated players that their spectator is gone
3418 if ply.Spectating then
3419 ply:Spectator( "End", { ply:GetObserverTarget() } )
3420 ply.Spectating = nil
3421 end
3422
3423 -- When they're racing, close the match
3424 if ply.Race then
3425 ply.Race:Abandon( ply )
3426 end
3427
3428 -- Clear their bot and sync data
3429 ply:ClearStrafeStats()
3430 ply:CleanFrames()
3431
3432 -- Check if a vote is going on
3433 if RTV.VotePossible then return end
3434
3435 -- If not, remove their vote
3436 if ply.Rocked then
3437 RTV.Votes = RTV.Votes - 1
3438 end
3439
3440 -- And check if the vote passes now
3441 local Count = RTV.GetVotable( ply )
3442 if Count > 0 then
3443 RTV.Required = math.ceil( Count * RTV.Fraction )
3444
3445 if RTV.Votes >= RTV.Required then
3446 RTV.StartVote()
3447 end
3448 end
3449end
3450hook.Add( "PlayerDisconnected", "PlayerDisconnect", PlayerDisconnect )
3451
3452
3453
3454-- RTV System
3455RTV.Identifier = "MapCountdown"
3456RTV.Version = 1
3457RTV.ListMax = 5
3458RTV.VoteCount = 7
3459RTV.Votes = 0
3460RTV.MapRepeat = 6
3461RTV.MinLimitations = 4
3462RTV.Fraction = 11 / 20
3463RTV.VoteList = {}
3464RTV.VotePossible = false
3465RTV.RandomizeTie = true
3466
3467RTV.VoteTime = 30
3468RTV.VoteTimeEnd = 0
3469RTV.Extends = 0
3470RTV.Length = 45 * 60
3471RTV.DefaultExtend = 20 * 60
3472RTV.WaitPeriod = 5 * 60
3473RTV.CheckInterval = 0.5 * 60
3474RTV.BroadcastInterval = 30 / 10
3475
3476if not RTV.Initialized then
3477 RTV.TimeNotify = { { 15 }, { 10 }, { 5 }, { 2 }, { 1 } }
3478
3479 RTV.Initialized = ST()
3480 RTV.Begin = RTV.Initialized
3481 RTV.End = RTV.Begin + RTV.Length
3482end
3483
3484RTV.Func = {}
3485RTV.AutoExtend = {}
3486RTV.Nominations = {}
3487RTV.LatestList = {}
3488
3489--[[
3490 Description: Starts the RTV system
3491--]]
3492function RTV:Start()
3493 -- Make sure there's only one RTV timer running
3494 if timer.Exists( self.Identifier ) then
3495 timer.Remove( self.Identifier )
3496 end
3497
3498 -- Create a timer
3499 timer.Create( self.Identifier, self.Length, 1, self.StartVote )
3500 timer.Create( self.Identifier .. "Hourglass", self.CheckInterval, 0, self.TimeCheck )
3501
3502 -- Set initialization fields for lifetime calculation
3503 self.Begin = ST()
3504 self.End = self.Begin + self.Length
3505
3506 -- Populate the vote list with 0 votes
3507 for i = 1, self.VoteCount do
3508 self.VoteList[ i ] = 0
3509 end
3510
3511 -- Load all necessary data
3512 self:Load()
3513
3514 -- Crack up the random generator to throw in a little less than pseudo-randoms
3515 RTV.TrueRandom( 1, 5 )
3516end
3517
3518--[[
3519 Description: Loads data required for the RTV system
3520--]]
3521function RTV:Load()
3522 file.CreateDir( Config.BaseType .. "/" )
3523
3524 -- Load in or write the map version
3525 if not file.Exists( Config.BaseType .. "/maplistversion.txt", "DATA" ) then
3526 file.Write( Config.BaseType .. "/maplistversion.txt", tostring( self.Version ) )
3527 else
3528 self.Version = tonumber( file.Read( Config.BaseType .. "/maplistversion.txt", "DATA" ) )
3529 end
3530
3531 -- Create a dummy file if it's blank
3532 local dummy = {}
3533 for i = 1, RTV.MapRepeat do dummy[ i ] = "Dummy" end
3534
3535 if not file.Exists( Config.BaseType .. "/maptracker.txt", "DATA" ) then
3536 file.Write( Config.BaseType .. "/maptracker.txt", util.TableToJSON( dummy ) )
3537 end
3538
3539 -- Check file content
3540 local content = file.Read( Config.BaseType .. "/maptracker.txt", "DATA" )
3541 if not content or content == "" then return end
3542
3543 -- Try to deserialize
3544 local tab = util.JSONToTable( content )
3545 if not tab or #tab != RTV.MapRepeat then
3546 return file.Write( Config.BaseType .. "/maptracker.txt", util.TableToJSON( dummy ) )
3547 end
3548
3549 -- If we're going back to the same map, don't keep adding to the list
3550 if tab[ 1 ] == Timer:GetMap() then return end
3551
3552 -- Insert at front and remove at the back
3553 table.insert( tab, 1, Timer:GetMap() )
3554 table.remove( tab, RTV.MapRepeat + 1 )
3555
3556 -- Update the table
3557 self.LatestList = tab
3558
3559 -- Finally write to file
3560 file.Write( Config.BaseType .. "/maptracker.txt", util.TableToJSON( self.LatestList ) )
3561end
3562
3563--[[
3564 Description: Starts the vote
3565 Used by: RTV timer only
3566--]]
3567function RTV.StartVote()
3568 if RTV.VotePossible then return end
3569
3570 -- Let everyone know we just started a vote
3571 RTV.VotePossible = true
3572 RTV.Selections = {}
3573 Core.Print( nil, "Notification", Core.Text( "VoteStart" ) )
3574
3575 -- Iterate over the nomination table and categorize it by vote count
3576 local MapList, MaxCount = {}, 1
3577 for map,voters in pairs( RTV.Nominations ) do
3578 local amount = 0
3579 for _,v in pairs( voters ) do
3580 if IsValid( v ) then
3581 amount = amount + 1
3582 end
3583 end
3584
3585 -- If we've got an entry already, expand, otherwise create it
3586 local count = MapList[ amount ] and #MapList[ amount ]
3587 if not count then
3588 MapList[ amount ] = { map }
3589 else
3590 MapList[ amount ][ count + 1 ] = map
3591 end
3592
3593 -- Increase max count if necessary
3594 if amount > MaxCount then
3595 MaxCount = amount
3596 end
3597 end
3598
3599 -- Loop over the most important nominations
3600 for i = MaxCount, 1, -1 do
3601 if MapList[ i ] then
3602 for j = 1, #MapList[ i ] do
3603 if #RTV.Selections >= RTV.ListMax then break end
3604
3605 -- Add the nomination to the list
3606 RTV.Selections[ #RTV.Selections + 1 ] = MapList[ i ][ j ]
3607 end
3608 end
3609 end
3610
3611 -- If we haven't had sufficient nominations, gather some random maps
3612 if #RTV.Selections < 5 and Timer.Maps > 0 then
3613 -- Copy the base table and remove already nominated entries
3614 local copy = table.Copy( Maps )
3615 for i = 1, #RTV.Selections do
3616 copy[ RTV.Selections[ i ] ] = nil
3617 end
3618
3619 -- Gather all the maps in a sortable array
3620 local temp = {}
3621 for map,data in pairs( copy ) do
3622 temp[ #temp + 1 ] = { Map = map, Plays = data.nPlays or 0 }
3623 end
3624
3625 -- Sort the table by plays
3626 table.SortByMember( temp, "Plays", true )
3627
3628 -- Get the 25 least played maps in a separate table
3629 local limit = {}
3630 for i = 1, 25 do
3631 limit[ i ] = temp[ i ]
3632 end
3633
3634 -- Finally add random entries
3635 for _,data in RandomPairs( limit ) do
3636 local map = data.Map
3637 if #RTV.Selections >= RTV.ListMax then break end
3638 if HAS( RTV.Selections, map ) or map == Timer:GetMap() then continue end
3639 if HAS( RTV.LatestList, map ) then continue end
3640
3641 -- Add the random map to the list
3642 RTV.Selections[ #RTV.Selections + 1 ] = { map, RTV.GetMapData( map ) }
3643 end
3644 end
3645
3646 -- Create a sortable table
3647 local sorted = {}
3648 for i = 1, #RTV.Selections do
3649 local item = RTV.Selections[ i ]
3650 if type( item ) == "table" then
3651 sorted[ #sorted + 1 ] = { Map = item[ 1 ], Plays = item[ 2 ][ 3 ], ListID = i }
3652 end
3653 end
3654
3655 -- Check if we have maps to sort
3656 if #sorted > 0 then
3657 -- Sort the table with ascending plays
3658 table.SortByMember( sorted, "Plays", true )
3659
3660 -- Reset the current table
3661 local offset
3662 for i = 1, #RTV.Selections do
3663 if type( RTV.Selections[ i ] ) == "table" then
3664 if not offset then offset = i end
3665 RTV.Selections[ i ] = nil
3666 end
3667 end
3668
3669 -- Overwrite table entries with re-sorted entries
3670 for i = 1, #sorted do
3671 if not offset then break end
3672 RTV.Selections[ offset + i - 1 ] = sorted[ i ].Map
3673 end
3674 end
3675
3676 -- Create a new table with only map data to be sent
3677 local RTVSend = {}
3678 for i = 1, #RTV.Selections do
3679 RTVSend[ #RTVSend + 1 ] = RTV.GetMapData( RTV.Selections[ i ] )
3680 end
3681
3682 -- Make the list accessible from the RTV object and set the ending time
3683 RTV.VoteTimeEnd = ST() + RTV.VoteTime
3684 RTV.Sent = RTVSend
3685 RTV.Sent.Countdown = math.Clamp( RTV.VoteTimeEnd - ST(), 0, RTV.VoteTime )
3686
3687 -- Broadcast the compiled list and start a timer
3688 timer.Simple( RTV.VoteTime + 1, RTV.EndVote )
3689 Core.Broadcast( "RTV/List", RTV.Sent )
3690
3691 -- Distribute the instant votes
3692 timer.Simple( 0.5, function()
3693 local extend = {}
3694 for p,v in pairs( RTV.AutoExtend ) do
3695 if v then
3696 extend[ #extend + 1 ] = p
3697 end
3698 end
3699
3700 if #extend > 0 then
3701 Core.Send( extend, "RTV/InstantVote", 6 )
3702 end
3703
3704 for map,voters in pairs( RTV.Nominations ) do
3705 for id,data in pairs( RTV.Sent ) do
3706 if id == "Countdown" then continue end
3707 if data[ 1 ] == map then
3708 local out = {}
3709 for _,p in pairs( voters ) do
3710 if not RTV.AutoExtend[ p ] then
3711 out[ #out + 1 ] = p
3712 end
3713 end
3714
3715 Core.Send( out, "RTV/InstantVote", id )
3716 end
3717 end
3718 end
3719 end )
3720
3721 -- Check broadcast timer
3722 if timer.Exists( RTV.Identifier .. "Broadcast" ) then
3723 timer.Remove( RTV.Identifier .. "Broadcast" )
3724 end
3725
3726 -- Create one with iterations that stop before the timer runs out
3727 timer.Create( RTV.Identifier .. "Broadcast", RTV.BroadcastInterval, RTV.VoteTime / RTV.BroadcastInterval - 1, function()
3728 NetPrepare( "RTV/VoteList", RTV.VoteList ):Broadcast()
3729 end )
3730end
3731
3732--[[
3733 Description: Ends the vote and decides what won (a map or extend or even random)
3734--]]
3735function RTV.EndVote()
3736 if RTV.CancelVote then
3737 return RTV:ResetVote( "Yes", 2, false, "VoteCancelled" )
3738 end
3739
3740 -- Trigger finalization (bots)
3741 GAMEMODE:UnloadGamemode( "VoteEnd" )
3742
3743 local nMax, nTotal, nWin = 0, 0, -1
3744 for i = 1, 7 do
3745 if RTV.VoteList[ i ] and RTV.VoteList[ i ] > nMax then
3746 nMax = RTV.VoteList[ i ]
3747 nWin = i
3748 end
3749
3750 nTotal = nTotal + RTV.VoteList[ i ]
3751 end
3752
3753 -- If enabled, pick a random one if there's duplicates
3754 if RTV.RandomizeTie then
3755 local votes = {}
3756 for i = 1, 7 do
3757 if RTV.VoteList[ i ] == nMax then
3758 votes[ #votes + 1 ] = i
3759 end
3760 end
3761
3762 if #votes > 1 then
3763 nWin = votes[ RTV.TrueRandom( 1, #votes ) ]
3764 Core.Print( nil, "Notification", Core.Text( "VoteSameVotes", "#" .. string.Implode( ", #", votes ), nWin ) )
3765 end
3766 end
3767
3768 -- Execute winner function
3769 if nWin <= 0 then
3770 nWin = RTV.TrueRandom( 1, 5 )
3771 elseif nWin == 6 then
3772 Core.Print( nil, "Notification", Core.Text( "VoteExtend", RTV.DefaultExtend / 60 ) )
3773 return RTV:ResetVote( nil, 1, true, nil )
3774 elseif nWin == 7 then
3775 RTV.VotePossible = false
3776
3777 if Timer.Maps > 0 then
3778 local ListMap, ListPlays = {}, {}
3779 for map,data in pairs( Maps ) do
3780 ListMap[ #ListMap + 1 ] = map
3781 ListPlays[ #ListPlays + 1 ] = data["nPlays"]
3782 end
3783
3784 local minId, minValue, thisMap = ListMap[ 1 ], ListPlays[ 1 ], Timer:GetMap()
3785 for i = 2, #ListPlays do
3786 if ListPlays[ i ] < minValue and ListMap[ i ] != thisMap then
3787 minId = ListMap[ i ]
3788 minValue = ListPlays[ i ]
3789 end
3790 end
3791
3792 if minId and minValue and Maps[ minId ] then
3793 nWin = 1
3794 RTV.Selections[ nWin ] = minId
3795 else
3796 nWin = RTV.TrueRandom( 1, 5 )
3797 end
3798 else
3799 nWin = RTV.TrueRandom( 1, 5 )
3800 end
3801 end
3802
3803 -- Get the map from the selection table
3804 local szMap = RTV.Selections[ nWin ]
3805 if not szMap or not type( szMap ) == "string" then
3806 return Core.Print( nil, "Notification", Core.Text( "VoteMissing", szMap ) )
3807 end
3808
3809 if not RTV.IsAvailable( szMap ) then
3810 Core.Print( nil, "Notification", Core.Text( "VoteMissing", szMap ) )
3811 else
3812 Core.Print( nil, "Notification", Core.Text( "VoteChange", szMap ) )
3813 end
3814
3815 -- Backup reset for if we don't change
3816 timer.Simple( 10, function()
3817 RTV:ResetVote( "Yes", 1, false, "VoteFailure" )
3818 end )
3819
3820 -- Finally change level
3821 Core.ChangeStarted = szMap
3822 timer.Simple( 5, function()
3823 GAMEMODE:UnloadGamemode( "Change" )
3824 RunConsoleCommand( "changelevel", Core.ChangeStarted )
3825 Core.ChangeStarted = nil
3826 end )
3827
3828 -- Logging print
3829 print( "[RTV] Map changed to: " .. (Core.ChangeStarted or "Unknown") )
3830end
3831
3832--[[
3833 Description: Resets the vote data according to the vote type
3834--]]
3835function RTV:ResetVote( szCancel, nMult, bExtend, szMsg )
3836 nMult = nMult or 1
3837
3838 if szCancel and szCancel == "Yes" then
3839 self.CancelVote = nil
3840 end
3841
3842 self.VotePossible = false
3843 self.Selections = {}
3844
3845 self.Begin = ST()
3846 self.End = self.Begin + (nMult * self.DefaultExtend)
3847
3848 self.Votes = 0
3849 for i = 1, self.VoteCount do
3850 self.VoteList[ i ] = 0
3851 end
3852
3853 for _,d in pairs( self.TimeNotify ) do
3854 d[ 2 ] = nil
3855 end
3856
3857 if bExtend then
3858 self.Extends = self.Extends + 1
3859 end
3860
3861 for _,p in pairs( player.GetHumans() ) do
3862 p.Rocked = nil
3863 p.LastVotedID = nil
3864 p.ResentVote = nil
3865 end
3866
3867 if timer.Exists( self.Identifier ) then
3868 timer.Remove( self.Identifier )
3869 end
3870
3871 timer.Create( self.Identifier, nMult * self.DefaultExtend, 1, self.StartVote )
3872
3873 if szMsg then
3874 Core.Print( nil, "Notification", Core.Text( szMsg ) )
3875 end
3876end
3877
3878--[[
3879 Description: Changes the time left on the vote
3880--]]
3881function RTV.ChangeTime( nMins )
3882 -- Make sure there's only one RTV timer running
3883 if timer.Exists( RTV.Identifier ) then
3884 timer.Remove( RTV.Identifier )
3885 end
3886
3887 timer.Create( RTV.Identifier, nMins * 60, 1, RTV.StartVote )
3888
3889 RTV.End = ST() + nMins * 60
3890
3891 for _,d in pairs( RTV.TimeNotify ) do
3892 d[ 2 ] = nil
3893 end
3894
3895 for i = 1, #RTV.TimeNotify do
3896 local item = RTV.TimeNotify[ i ]
3897 if nMins * 60 < item[ 1 ] * 60 then
3898 item[ 2 ] = true
3899 end
3900 end
3901end
3902Core.RTVChangeTime = RTV.ChangeTime
3903
3904--[[
3905 Description: Broadcasts a timeleft notification to every connected player
3906 Notes: Runs on a timer
3907--]]
3908function RTV.TimeCheck()
3909 local remaining = RTV.End - ST()
3910 for i = 1, #RTV.TimeNotify do
3911 local item = RTV.TimeNotify[ i ]
3912 if remaining < item[ 1 ] * 60 and not item[ 2 ] then
3913 local text = remaining < 60 and "Less than 1 minute remaining" or ((remaining >= 60 and remaining < 120) and "1 minute remaining" or math.floor( remaining / 60 ) .. " minutes remaining")
3914 NetPrepare( "Notify", { "Notification", text, "hourglass", 10, text } ):Broadcast()
3915
3916 item[ 2 ] = true
3917 break
3918 end
3919 end
3920end
3921
3922--[[
3923 Description: Get the amount of people that can actually vote in the server
3924--]]
3925function RTV.GetVotable( exclude, plys )
3926 local n, ps = 0, {}
3927
3928 for _,p in pairs( player.GetHumans() ) do
3929 if p == exclude then
3930 continue
3931 elseif p.AFKTab and p.AFKTab.Points[ p ] < 2 then
3932 continue
3933 elseif StylePoints[ Styles.Normal ] and (not StylePoints[ Styles.Normal ][ p.UID ] or StylePoints[ Styles.Normal ][ p.UID ] == 0) then
3934 if p.Style != Styles.Normal then
3935 continue
3936 elseif p.Record == 0 and #player.GetHumans() > RTV.MinLimitations then
3937 continue
3938 end
3939 end
3940
3941 n = n + 1
3942 ps[ #ps + 1 ] = p
3943 end
3944
3945 return plys and ps or n
3946end
3947
3948
3949--[[
3950 Description: Triggers a vote on the player if possible
3951--]]
3952function RTV.Func.Vote( ply )
3953 if ply.RTVLimit and ST() - ply.RTVLimit < 60 then
3954 return Core.Print( ply, "Notification", Core.Text( "VoteLimit", math.ceil( 60 - (ST() - ply.RTVLimit) ) ) )
3955 elseif ply.Rocked then
3956 return Core.Print( ply, "Notification", Core.Text( "VoteAlready" ) )
3957 elseif RTV.VotePossible then
3958 return Core.Print( ply, "Notification", Core.Text( "VotePeriod" ) )
3959 --elseif ST() - RTV.Begin < RTV.WaitPeriod then
3960 -- return Core.Print( ply, "Notification", Core.Text( "VoteLimited", string.format( "%.1f", (RTV.WaitPeriod - (ST() - RTV.Begin)) / 60 ) ) )
3961 elseif StylePoints[ Styles.Normal ] and (not StylePoints[ Styles.Normal ][ ply.UID ] or StylePoints[ Styles.Normal ][ ply.UID ] == 0) then
3962 if ply.Style != Styles.Normal then
3963 return Core.Print( ply, "Notification", Core.Text( "VoteLimitPlay" ) )
3964 elseif ply.Record == 0 and #player.GetHumans() > RTV.MinLimitations then
3965 return Core.Print( ply, "Notification", Core.Text( "VoteLimitPlay" ) )
3966 end
3967 end
3968
3969 ply.RTVLimit = ST()
3970 ply.Rocked = true
3971
3972 RTV.Votes = RTV.Votes + 1
3973 RTV.Required = math.ceil( RTV.GetVotable() * RTV.Fraction )
3974
3975 local nVotes = RTV.Required - RTV.Votes
3976 Core.Print( nil, "Notification", Core.Text( "VotePlayer", ply:Name(), nVotes, nVotes == 1 and "vote" or "votes", math.ceil( (RTV.Votes / RTV.Required) * 100 ) ) )
3977
3978 if RTV.Votes >= RTV.Required then
3979 RTV.StartVote()
3980 end
3981end
3982
3983--[[
3984 Description: Revokes a vote on the player if there is any
3985--]]
3986function RTV.Func.Revoke( ply )
3987 if RTV.VotePossible then
3988 return Core.Print( ply, "Notification", Core.Text( "VotePeriod" ) )
3989 end
3990
3991 if ply.Rocked then
3992 ply.Rocked = false
3993
3994 RTV.Votes = RTV.Votes - 1
3995 RTV.Required = math.ceil( RTV.GetVotable() * RTV.Fraction )
3996
3997 local nVotes = RTV.Required - RTV.Votes
3998 Core.Print( nil, "Notification", Core.Text( "VoteRevoke", ply:Name(), nVotes, nVotes == 1 and "vote" or "votes" ) )
3999 else
4000 Core.Print( ply, "Notification", Core.Text( "VoteRevokeFail" ) )
4001 end
4002end
4003
4004--[[
4005 Description: Nominates a map
4006 Notes: Whole lot of extra logic for sorting the maps
4007--]]
4008function RTV.Func.Nominate( ply, szMap )
4009 local szIdentifier = "Nomination"
4010 local varArgs = { ply:Name(), szMap }
4011
4012 if #player.GetHumans() > RTV.MinLimitations and HAS( RTV.LatestList, szMap ) then
4013 local at = 1
4014 for id,map in pairs( RTV.LatestList ) do
4015 if map == szMap then
4016 at = id
4017 break
4018 end
4019 end
4020
4021 return Core.Print( ply, "Notification", Core.Text( "NominateRecent", at - 1 ) )
4022 end
4023
4024 if ply.NominatedMap and ply.NominatedMap != szMap then
4025 if RTV.Nominations[ ply.NominatedMap ] then
4026 for id,p in pairs( RTV.Nominations[ ply.NominatedMap ] ) do
4027 if p == ply then
4028 table.remove( RTV.Nominations[ ply.NominatedMap ], id )
4029
4030 if #RTV.Nominations[ ply.NominatedMap ] == 0 then
4031 RTV.Nominations[ ply.NominatedMap ] = nil
4032 end
4033
4034 szIdentifier = "NominationChange"
4035 varArgs = { ply:Name(), ply.NominatedMap, szMap }
4036
4037 break
4038 end
4039 end
4040 end
4041 elseif ply.NominatedMap and ply.NominatedMap == szMap then
4042 return Core.Print( ply, "Notification", Core.Text( "NominationAlready" ) )
4043 end
4044
4045 if not RTV.Nominations[ szMap ] then
4046 RTV.Nominations[ szMap ] = { ply }
4047 ply.NominatedMap = szMap
4048 Core.Print( nil, "Notification", Core.Text( szIdentifier, varArgs ) )
4049 elseif type( RTV.Nominations ) == "table" then
4050 local Included = false
4051 for _,p in pairs( RTV.Nominations[ szMap ] ) do
4052 if p == ply then Included = true break end
4053 end
4054
4055 if not Included then
4056 table.insert( RTV.Nominations[ szMap ], ply )
4057 ply.NominatedMap = szMap
4058 Core.Print( nil, "Notification", Core.Text( szIdentifier, varArgs ) )
4059 else
4060 return Core.Print( ply, "Notification", Core.Text( "NominationAlready" ) )
4061 end
4062 end
4063end
4064
4065--[[
4066 Description: Returns a list of who has voted and who hasn't voted
4067--]]
4068function RTV.Func.Who( ply )
4069 local Voted = {}
4070 local NotVoted = {}
4071
4072 for _,p in pairs( RTV.GetVotable( nil, true ) ) do
4073 if p.Rocked then
4074 table.insert( Voted, p:Name() )
4075 else
4076 table.insert( NotVoted, p:Name() )
4077 end
4078 end
4079
4080 RTV.Required = math.ceil( RTV.GetVotable() * RTV.Fraction )
4081 Core.Print( ply, "Notification", Core.Text( "VoteList", RTV.Required, #Voted, string.Implode( ", ", Voted ), #NotVoted, string.Implode( ", ", NotVoted ) ) )
4082end
4083
4084--[[
4085 Description: Checks how many votes are left before the map changes
4086--]]
4087function RTV.Func.Check( ply )
4088 RTV.Required = math.ceil( RTV.GetVotable() * RTV.Fraction )
4089
4090 local nVotes = RTV.Required - RTV.Votes
4091 Core.Print( ply, "Notification", Core.Text( "VoteCheck", nVotes, nVotes == 1 and "vote" or "votes" ) )
4092end
4093
4094--[[
4095 Description: Returns the time remaining before a change of maps
4096--]]
4097function RTV.Func.Left( ply )
4098 Core.Print( ply, "Notification", Core.Text( "MapTimeLeft", Timer.Convert( RTV.End - ST() ) ) )
4099end
4100
4101--[[
4102 Description: Resends the voting screen to the player
4103--]]
4104function RTV.Func.Revote( ply, bGet )
4105 if bGet then return RTV.VotePossible end
4106 if not RTV.VotePossible then return Core.Print( ply, "Notification", Core.Text( "VotePeriodActive" ) ) end
4107 ply.ResentVote = true
4108
4109 RTV.Sent.Countdown = math.Clamp( RTV.VoteTimeEnd - ST(), 0, RTV.VoteTime )
4110 Core.Send( ply, "RTV/List", RTV.Sent )
4111end
4112
4113--[[
4114 Description: Gets a type of map requested by the player
4115--]]
4116function RTV.Func.MapFunc( ply, key )
4117 if Timer.Maps == 0 then return end
4118
4119 if key == "playinfo" then
4120 Core.Print( ply, "General", Core.Text( "TimerMapsInfo" ) )
4121 elseif key == "leastplayed" then
4122 local temp = {}
4123 for map,data in pairs( Maps ) do
4124 temp[ #temp + 1 ] = { Map = map, Plays = data.nPlays or 0 }
4125 end
4126
4127 table.SortByMember( temp, "Plays", true )
4128
4129 local str = {}
4130 for i = 1, 5 do
4131 str[ i ] = temp[ i ].Map .. " (" .. temp[ i ].Plays .. " plays)"
4132 end
4133
4134 Core.Print( ply, "General", Core.Text( "TimerMapsDisplay", "Least", string.Implode( ", ", str ) ) )
4135 elseif key == "mostplayed" or key == "overplayed" then
4136 local temp = {}
4137 for map,data in pairs( Maps ) do
4138 temp[ #temp + 1 ] = { Map = map, Plays = data.nPlays or 0 }
4139 end
4140
4141 table.SortByMember( temp, "Plays", false )
4142
4143 local str = {}
4144 for i = 1, 5 do
4145 str[ i ] = temp[ i ].Map .. " (" .. temp[ i ].Plays .. " plays)"
4146 end
4147
4148 Core.Print( ply, "General", Core.Text( "TimerMapsDisplay", "Most", string.Implode( ", ", str ) ) )
4149 elseif key == "lastplayed" or key == "lastmaps" then
4150 local temp = {}
4151 for map,data in pairs( Maps ) do
4152 temp[ #temp + 1 ] = { Map = map, Date = data.szDate }
4153 end
4154
4155 table.SortByMember( temp, "Date", false )
4156
4157 local str = {}
4158 for i = 1, 5 do
4159 str[ i ] = temp[ i ].Map .. " (" .. temp[ i ].Date .. ")"
4160 end
4161
4162 Core.Print( ply, "General", Core.Text( "TimerMapsDisplay", "Last", string.Implode( ", ", str ) ) )
4163 elseif key == "randommap" then
4164 for map,data in RandomPairs( Maps ) do
4165 Core.Print( ply, "General", Core.Text( "TimerMapsRandom", map ) )
4166 break
4167 end
4168 end
4169end
4170
4171--[[
4172 Description: Shows which map you have nominated
4173--]]
4174function RTV.Func.Which( ply )
4175 Core.Print( ply, "Notification", ply.NominatedMap and Core.Text( "MapNominated", "", ply.NominatedMap ) or Core.Text( "MapNominated", "n't", "a map" ) )
4176end
4177
4178--[[
4179 Description: Shows all nominated maps
4180--]]
4181function RTV.Func.Nominations( ply )
4182 local MapList, MaxCount = {}, 1
4183 for map,voters in pairs( RTV.Nominations ) do
4184 local plys = { map }
4185 for _,v in pairs( voters ) do
4186 if IsValid( v ) then
4187 plys[ #plys + 1 ] = v:Name()
4188 end
4189 end
4190
4191 -- If we've got an entry already, expand, otherwise create it
4192 local amount = #plys - 1
4193 local count = MapList[ amount ] and #MapList[ amount ]
4194 if not count then
4195 MapList[ amount ] = { plys }
4196 else
4197 MapList[ amount ][ count + 1 ] = plys
4198 end
4199
4200 -- Increase max count if necessary
4201 if amount > MaxCount then
4202 MaxCount = amount
4203 end
4204 end
4205
4206 -- Loop over the most important nominations
4207 local str, add = Core.Text( "MapNominations" )
4208 for i = MaxCount, 1, -1 do
4209 if MapList[ i ] then
4210 for j = 1, #MapList[ i ] do
4211 str = str .. "- " .. table.remove( MapList[ i ][ j ], 1 ) .. " (By " .. i .. " player(s): " .. string.Implode( ", ", MapList[ i ][ j ] ) .. ")\n"
4212 add = true
4213 end
4214 end
4215 end
4216
4217 -- Print the message out
4218 Core.Print( ply, "Notification", add and (str .. Core.Text( "MapNominationChance" )) or Core.Text( "MapNominationsNone" ) )
4219end
4220
4221--[[
4222 Description: Revokes a player map nomination
4223--]]
4224function RTV.Func.Denominate( ply )
4225 if not ply.NominatedMap then
4226 return Core.Print( ply, "Notification", Core.Text( "MapNominationNone" ) )
4227 end
4228
4229 if RTV.Nominations[ ply.NominatedMap ] then
4230 for id,p in pairs( RTV.Nominations[ ply.NominatedMap ] ) do
4231 if p == ply then
4232 table.remove( RTV.Nominations[ ply.NominatedMap ], id )
4233
4234 if #RTV.Nominations[ ply.NominatedMap ] == 0 then
4235 RTV.Nominations[ ply.NominatedMap ] = nil
4236 end
4237
4238 break
4239 end
4240 end
4241 end
4242
4243 ply.NominatedMap = nil
4244
4245 Core.Print( ply, "Notification", Core.Text( "MapNominationRevoke" ) )
4246end
4247
4248--[[
4249 Description: Sets the player to automatically vote extend
4250--]]
4251function RTV.Func.Extend( ply )
4252 RTV.AutoExtend[ ply ] = not RTV.AutoExtend[ ply ]
4253
4254 Core.Print( ply, "Notification", Core.Text( "MapAutoExtend", not RTV.AutoExtend[ ply ] and "no longer " or "" ) )
4255end
4256
4257--[[
4258 Description: The function that triggers the RTV.Func's
4259--]]
4260function PLAYER:RTV( szType, args )
4261 if RTV.Func[ szType ] then
4262 return RTV.Func[ szType ]( self, args )
4263 end
4264end
4265
4266
4267--[[
4268 Description: Process a received vote
4269 Used by: Called from network
4270--]]
4271function RTV.ReceiveVote( ply, varArgs )
4272 local nVote, nOld = varArgs[ 1 ], ply.RTVOldVote
4273 if not RTV.VotePossible or not nVote then return end
4274 if ply.LastVotedID == nVote then return end
4275
4276 if not nOld and ply.ResentVote and ply.LastVotedID then
4277 nOld = ply.LastVotedID
4278 ply.ResentVote = nil
4279 end
4280
4281 ply.LastVotedID = nVote
4282
4283 local nAdd = 1
4284 if not nOld then
4285 if nVote < 1 or nVote > 7 then return end
4286 if not RTV.VoteList[ nVote ] then RTV.VoteList[ nVote ] = 0 end
4287 RTV.VoteList[ nVote ] = RTV.VoteList[ nVote ] + nAdd
4288 ply.RTVOldVote = nVote
4289 else
4290 if nVote < 1 or nVote > 7 or nOld < 1 or nOld > 7 then return end
4291 if not RTV.VoteList[ nVote ] then RTV.VoteList[ nVote ] = 0 end
4292 if not RTV.VoteList[ nOld ] then RTV.VoteList[ nOld ] = 0 end
4293 RTV.VoteList[ nVote ] = RTV.VoteList[ nVote ] + nAdd
4294 RTV.VoteList[ nOld ] = RTV.VoteList[ nOld ] - nAdd
4295 if RTV.VoteList[ nOld ] < 0 then RTV.VoteList[ nOld ] = 0 end
4296 ply.RTVOldVote = nVote
4297 end
4298
4299 NetPrepare( "RTV/VoteList", RTV.VoteList ):Broadcast()
4300end
4301Core.Register( "Global/Vote", RTV.ReceiveVote )
4302
4303--[[
4304 Description: Sends the map list to a player
4305 Notes: Encodes it here since it might take a while before anyone needs a new map list
4306--]]
4307local EncodedData, EncodedLength
4308function RTV.GetMapList( ply, varArgs )
4309 if varArgs[ 1 ] != RTV.Version then
4310 if not EncodedData or not EncodedLength then
4311 EncodedData = util.Compress( util.TableToJSON( { Maps, RTV.Version, Timer.Maps } ) )
4312 EncodedLength = #EncodedData
4313 end
4314
4315 if not EncodedData or not EncodedLength then
4316 Core.Print( ply, "Notification", Core.Text( "MiscMissingMapList" ) )
4317 else
4318 net.Start( "BinaryTransfer" )
4319 net.WriteString( "List" )
4320 net.WriteUInt( EncodedLength, 32 )
4321 net.WriteData( EncodedData, EncodedLength )
4322 net.Send( ply )
4323 end
4324 end
4325end
4326Core.Register( "Global/MapList", RTV.GetMapList )
4327
4328--[[
4329 Description: Update the version number and increment it
4330--]]
4331function RTV:UpdateVersion( nAmount )
4332 self.Version = self.Version + (nAmount or 1)
4333 file.Write( Config.BaseType .. "/maplistversion.txt", tostring( self.Version ) )
4334end
4335
4336--[[
4337 Description: Checks if the map exists on the disk
4338--]]
4339function RTV.IsAvailable( szMap )
4340 return file.Exists( "maps/" .. szMap .. ".bsp", "GAME" )
4341end
4342
4343--[[
4344 Description: Checks if the map exists in the loaded database table
4345--]]
4346function RTV.MapExists( szMap )
4347 return not not Maps[ szMap ]
4348end
4349
4350--[[
4351 Description: Returns the loaded data about a map
4352 Notes: Could add more but this is only necessary to be on the client itself
4353--]]
4354function RTV.GetMapData( szMap )
4355 local tab = Maps[ szMap ]
4356
4357 if tab then
4358 if Config.IsSurf then
4359 return { szMap, tab["nMultiplier"], tab["nPlays"], tab["nTier"] or 1, tab["nType"] or 0 }
4360 else
4361 return { szMap, tab["nMultiplier"], tab["nPlays"] }
4362 end
4363 else
4364 if Config.IsSurf then
4365 return { szMap, 1, 1, 1, 0 }
4366 else
4367 return { szMap, 1, 1 }
4368 end
4369 end
4370end
4371
4372
4373-- Zones
4374Zones.Type = {
4375 ["Normal Start"] = 0,
4376 ["Normal End"] = 1,
4377 ["Bonus Start"] = 2,
4378 ["Bonus End"] = 3,
4379 ["Anticheat"] = 4,
4380 ["Freestyle"] = 5,
4381 ["Normal AC"] = 6,
4382 ["Bonus AC"] = 7,
4383 ["Stage Start"] = 8,
4384 ["Stage End"] = 9,
4385 ["Restart Zone"] = 10,
4386 ["Velocity Zone"] = 11,
4387 ["Solid AC"] = 12
4388}
4389
4390-- The options that can be set
4391Zones.Options = {
4392 NoStartLimit = 1,
4393 NoSpeedLimit = 2,
4394 TelehopMap = 4,
4395 Checkpoints = 8
4396}
4397
4398-- Base settings
4399Zones.Settings = {
4400 MaxVelocity = 3500,
4401 MaxVelocityHard = 5000,
4402 UnlimitedVelocity = 100000
4403}
4404
4405-- Content tables
4406local ZoneCache = {}
4407local ZoneEnts = {}
4408local ClientEnts = {}
4409
4410
4411--[[
4412 Description: Loads all zones for this map from the database and parses them
4413--]]
4414function Zones.Load()
4415 Prepare(
4416 "SELECT nType, vPos1, vPos2 FROM game_zones WHERE szMap = {0}",
4417 { Timer:GetMap() },
4418 nil, true
4419 )( function( data, varArg, szError )
4420 if Core.Assert( data, "nType" ) then
4421 local makeNum, makeType = tonumber, util.StringToType
4422 for j = 1, #data do
4423 data[ j ]["nType"] = makeNum( data[ j ]["nType"] )
4424 data[ j ]["vPos1"] = makeType( data[ j ]["vPos1"], "Vector" )
4425 data[ j ]["vPos2"] = makeType( data[ j ]["vPos2"], "Vector" )
4426
4427 ZoneCache[ #ZoneCache + 1 ] = data[ j ]
4428 end
4429 end
4430 end )
4431end
4432
4433--[[
4434 Description: Sets up the zone entities themselves
4435 Used by: Map initialization and reloading
4436--]]
4437function Zones.Setup()
4438 Zones.BotPoints = {}
4439 Zones.StartPoints = {}
4440
4441 for i = 1, #ZoneCache do
4442 local zone = ZoneCache[ i ]
4443 local Type = zone["nType"]
4444 local P1, P2 = zone["vPos1"], zone["vPos2"]
4445 local M1 = (P1 + P2) / 2
4446
4447 -- Check for custom functions
4448 if Zones.CustomEnts[ Type ] then
4449 ZoneEnts[ #ZoneEnts + 1 ] = Zones.CustomEnts[ Type ]( zone )
4450
4451 continue
4452 end
4453
4454 -- Creates the entity
4455 local ent = ents.Create( "game_timer" )
4456 ent:SetPos( M1 )
4457 ent.min = P1
4458 ent.max = P2
4459 ent.zonetype = Type
4460 ent.truetype = Type
4461
4462 -- Sets start points for respawning
4463 if Type == Zones.Type["Normal Start"] then
4464 Zones.StartPoints[ #Zones.StartPoints + 1 ] = { P1, P2, M1 }
4465 Zones.BotPoints[ #Zones.BotPoints + 1 ] = Vector( M1.x, M1.y, P1.z )
4466 end
4467
4468 -- Custom zones with embedded data
4469 if Type >= 700 and Type <= 799 then
4470 ent.zonetype = Zones.Type["Velocity Zone"]
4471 ent.embedded = Type - 699 -- 700 will be embedded type 1 as the lowest, allowing 100 combinations
4472 elseif Type >= 600 and Type <= 699 then
4473 ent.zonetype = Zones.Type["Restart Zone"]
4474 ent.embedded = Type - 599 -- 600 will be embedded type 1 as the lowest, allowing 100 * 100 velocities
4475 elseif Type >= 500 and Type <= 599 then
4476 ent.zonetype = Zones.Type["Anticheat"]
4477 ent.embedded = Type - 499 -- 500 will be embedded type 1 as the lowest, allowing 100 styles max
4478 elseif Type >= 400 and Type <= 499 then
4479 ent.zonetype = Zones.Type["Stage End"]
4480 ent.embedded = Type - 399 -- 400 will be embedded type 1 as the lowest, allowing 100 stages max
4481 elseif Type >= 300 and Type <= 399 then
4482 ent.zonetype = Zones.Type["Stage Start"]
4483 ent.embedded = Type - 299 -- 300 will be embedded type 1 as the lowest, allowing 100 stages max
4484 elseif Type >= 200 and Type <= 299 then
4485 ent.zonetype = Zones.Type["Bonus End"]
4486 ent.embedded = Type - 198 -- 200 will be embedded type 2 as the lowest, allowing 101 bonuses max
4487 elseif Type >= 100 and Type <= 199 then
4488 ent.zonetype = Zones.Type["Bonus Start"]
4489 ent.embedded = Type - 98 -- 100 will be embedded type 2 as the lowest, allowing 101 bonuses max
4490 end
4491
4492 -- Create the entity
4493 ent:Spawn()
4494
4495 ZoneEnts[ #ZoneEnts + 1 ] = ent
4496 ClientEnts[ ent:EntIndex() ] = { ent.zonetype, ent.embedded }
4497 end
4498end
4499
4500--[[
4501 Description: Reloads all zone entities and re-broadcasts them
4502--]]
4503function Zones.Reload( nodb )
4504 for i = 1, #ZoneEnts do
4505 if IsValid( ZoneEnts[ i ] ) then
4506 ZoneEnts[ i ]:Remove()
4507 ZoneEnts[ i ] = nil
4508 end
4509 end
4510
4511 if not nodb then
4512 Core.CleanTable( ZoneCache )
4513 end
4514
4515 Core.CleanTable( ZoneEnts )
4516
4517 if nodb then
4518 Zones.Setup()
4519 Zones.BroadcastClientEnts()
4520 else
4521 Zones.Load()
4522 Zones.Setup()
4523 Zones.BroadcastClientEnts()
4524
4525 Core.BonusEntitySetup()
4526 end
4527end
4528Core.ReloadZones = Zones.Reload
4529
4530--[[
4531 Description: Translates a zone ID to a zone name
4532--]]
4533function Zones.GetName( n )
4534 for name,id in pairs( Zones.Type ) do
4535 if id == n then
4536 return name
4537 end
4538 end
4539
4540 return "Unknown"
4541end
4542Core.GetZoneName = Zones.GetName
4543
4544--[[
4545 Description: Gets the center point of a given zone with this type
4546 Used by: Commands 'end' and 'bonusend'
4547 Notes: This will not work if the zone is double
4548--]]
4549function Zones.GetCenterPoint( nType, nEmbed )
4550 for i = 1, #ZoneEnts do
4551 local zone = ZoneEnts[ i ]
4552 if IsValid( zone ) and zone.zonetype == nType then
4553 if nEmbed and nEmbed != zone.embedded then continue end
4554
4555 local pos = zone:GetPos()
4556 local height = zone.max.z - zone.min.z
4557
4558 pos.z = pos.z - (height / 2)
4559 return pos
4560 end
4561 end
4562end
4563
4564--[[
4565 Description: Checks if the player is inside of the given zone
4566 Used by: Bot forcing
4567--]]
4568function Zones.IsInside( ply, nType, nEmbed )
4569 for i = 1, #ZoneEnts do
4570 local zone = ZoneEnts[ i ]
4571 if IsValid( zone ) and zone.zonetype == nType then
4572 if nEmbed and nEmbed != zone.embedded then continue end
4573
4574 if table.HasValue( ents.FindInBox( zone.min, zone.max ), ply ) then
4575 return true
4576 end
4577 end
4578 end
4579end
4580Core.IsInsideZone = Zones.IsInside
4581
4582--[[
4583 Description: Gets the center point of a bonus zone if it exists
4584 Used by: Bonus resetting
4585--]]
4586function Zones.GetBonusPoint( nID )
4587 for i = 1, #ZoneEnts do
4588 local zone = ZoneEnts[ i ]
4589 if IsValid( zone ) then
4590 if zone.zonetype != Zones.Type["Bonus Start"] then continue end
4591
4592 local embed = zone.embedded and zone.embedded - 1 or 0
4593 if embed == nID then
4594 return { zone.min, zone.max, zone:GetPos() }
4595 end
4596 end
4597 end
4598end
4599
4600--[[
4601 Description: Gets all bonus ids
4602 Used by: Commands
4603--]]
4604function Zones.GetBonusIDs()
4605 local ids = {}
4606
4607 for i = 1, #ZoneEnts do
4608 local zone = ZoneEnts[ i ]
4609 if IsValid( zone ) then
4610 if zone.zonetype != Zones.Type["Bonus Start"] then continue end
4611
4612 ids[ #ids + 1 ] = zone.embedded and zone.embedded - 1 or 0
4613 end
4614 end
4615
4616 return ids
4617end
4618
4619--[[
4620 Description: Checks if the bonus is being done on the right style
4621--]]
4622function Zones.ValidateBonusStyle( ply, embedded )
4623 if not Core.IsValidBonus( ply.Style ) then return false end
4624
4625 return ply.Style - Styles.Bonus == (embedded and embedded - 1 or 0)
4626end
4627
4628--[[
4629 Description: Analyzes a zone and returns more data if available
4630--]]
4631function Zones:GetZoneInfo( zone )
4632 if not IsValid( zone ) then return "" end
4633 if self.Editor.Embedded[ zone.zonetype ] then
4634 return " (Data: " .. (zone.embedded or "Blank") .. ")"
4635 else
4636 return ""
4637 end
4638end
4639
4640--[[
4641 Description: Checks if an option is applied to a map
4642--]]
4643function Zones.IsOption( opt )
4644 return bit.band( Timer.Options, opt ) > 0
4645end
4646
4647--[[
4648 Description: Applies all options to the map
4649--]]
4650function Zones.CheckOptions()
4651 if Zones.IsOption( Zones.Options.NoSpeedLimit ) then
4652 RunConsoleCommand( "sv_maxvelocity", Zones.Settings.UnlimitedVelocity )
4653 else
4654 if Config.GameType == "bhophard" then
4655 Zones.Settings.MaxVelocity = Zones.Settings.MaxVelocityHard
4656 end
4657
4658 RunConsoleCommand( "sv_maxvelocity", Zones.Settings.MaxVelocity )
4659 end
4660end
4661
4662--[[
4663 Description: Finds the most appropriate spawn angle
4664--]]
4665function Zones.SetSpawnAngles()
4666 -- Set base value
4667 local top, selected = 0
4668
4669 -- Loop over the spawns
4670 for value,num in pairs( Timer.Spawns ) do
4671 if num > top then
4672 top = num
4673 selected = value
4674 end
4675 end
4676
4677 -- Get the top one
4678 if selected then
4679 Timer.BaseAngles = util.StringToType( selected, "Angle" )
4680 end
4681
4682 -- Let's convert stored data
4683 local tps = {}
4684 for _,item in pairs( Timer.Teleports ) do
4685 tps[ #tps + 1 ] = { util.StringToType( item[ 1 ], "Vector" ), util.StringToType( item[ 2 ], "Angle" ) }
4686 end
4687
4688 -- Create a temporary function
4689 local function FindNearestSpawn( at, tab )
4690 local order = {}
4691 for _,v in pairs( tab ) do
4692 local distance = (at - v[ 1 ]):Length()
4693 order[ #order + 1 ] = { Dist = distance, Vec = v[ 1 ], Ang = v[ 2 ] }
4694 end
4695
4696 -- Sort by distance
4697 table.SortByMember( order, "Dist", true )
4698
4699 -- Get the one that doesn't collide
4700 for i = 1, #order do
4701 local tr = util.TraceLine( { start = at, endpos = order[ i ].Vec } )
4702 if not tr.HitWorld then
4703 return order[ i ]
4704 end
4705 end
4706
4707 -- Otherwise, return the top entry
4708 return order[ 1 ]
4709 end
4710
4711 -- Now let's find the bonus zones
4712 Timer.BonusAngles = {}
4713
4714 -- Get the list
4715 for _,i in pairs( Zones.GetBonusIDs() ) do
4716 local data = Zones.GetBonusPoint( i )
4717
4718 if data then
4719 local near = FindNearestSpawn( data[ 3 ], tps )
4720
4721 if near then
4722 Timer.BonusAngles[ i ] = near.Ang
4723 end
4724 end
4725 end
4726end
4727
4728--[[
4729 Description: Processes a velocity zone touch
4730--]]
4731function PLAYER:ProcessVelocityZone( ent, endt )
4732 -- Validate whether the player is legit and in a bonus
4733 if not IsValid( self ) or not self.Style or not ent.embedded then return end
4734
4735 -- Extract all useful data from the embedded data
4736 local vel, frac = math.modf( ent.embedded )
4737 local ang, bfrac = math.modf( frac * 1000 )
4738 local bits = math.Round( bfrac * 10 )
4739
4740 -- See which EntityTouch event we want to handle
4741 if bit.band( bits, 1 ) > 0 then
4742 if endt then return end
4743 else
4744 if not endt then return end
4745 end
4746
4747 -- Check if double-boosting is disabled
4748 if bit.band( bits, 2 ) > 0 then
4749 if self:GetVelocity():Length2D() * 2 > vel * 100 then return end
4750 end
4751
4752 -- This means bonus only
4753 if bit.band( bits, 4 ) == 0 then
4754 if not self.Tb or not Core.IsValidBonus( self.Style ) then return end
4755 end
4756
4757 -- Now create and transform the vector
4758 local vec = Vector( 1, 0, 0 )
4759 vec:Mul( vel * 100 )
4760 vec:Rotate( Angle( 0, ang, 0 ) )
4761
4762 -- Apply the velocity to the player
4763 self:SetVelocity( vec )
4764end
4765
4766-- Custom entity initialization
4767Zones.CustomEnts = {}
4768Zones.CustomEnts[ Zones.Type["Solid AC"] ] = function( zone )
4769 local Type = zone["nType"]
4770 local P1, P2 = zone["vPos1"], zone["vPos2"]
4771 local M1 = (P1 + P2) / 2
4772
4773 -- Creates the entity
4774 local ent = ents.Create( "SolidBlockEnt" )
4775 ent:SetPos( P1 )
4776 ent.basemin = P1
4777 ent.basemax = P2
4778 ent.min = Vector( 0, 0, 0 )
4779 ent.max = P2 - P1
4780 ent.zonetype = Type
4781 ent.truetype = Type
4782 ent:Spawn()
4783
4784 return ent
4785end
4786
4787
4788-- Zone editor
4789Zones.Editor = {}
4790Zones.Editor.List = {}
4791
4792Zones.Editor.Embedded = {
4793 [2] = "Bonus Start",
4794 [3] = "Bonus End",
4795 [8] = "Stage Start",
4796 [9] = "Stage End",
4797 [4] = "Anticheat",
4798 [10] = "Restart Zone",
4799 [11] = "Velocity Zone"
4800}
4801
4802Zones.Editor.EmbeddedOffsets = {
4803 [2] = 96, -- 2 + 96 + 2 = 100 minimum at bonus 2
4804 [3] = 195, -- 3 + 195 + 2 = 200 minimum at bonus 2
4805 [8] = 291, -- 8 + 291 + 1 = 300 minimum at stage 1
4806 [9] = 390, -- 9 + 390 + 1 = 400 minimum at stage 1
4807 [4] = 495, -- 4 + 495 + 1 = 500 minimum at style 1
4808 [10] = 589, -- 10 + 589 + 1 = 600 minimum at speed 100
4809 [11] = 688 -- 11 + 688 + 1 = 700 minimum at combination 1
4810}
4811
4812Zones.Editor.Double = {
4813 [4] = "Anticheat",
4814 [5] = "Freestyle",
4815 [6] = "Normal AC",
4816 [7] = "Bonus AC",
4817 [8] = "Stage Start",
4818 [9] = "Stage End",
4819 [10] = "Restart Zone",
4820 [11] = "Velocity Zone"
4821}
4822
4823--[[
4824 Description: Start setting a zone with the given ID
4825--]]
4826function Zones.Editor:StartSet( ply, ID )
4827 -- Set default params
4828 local params = { "None" }
4829
4830 -- Avoid problems with people overriding zones they shouldn't be overriding
4831 if self.Double[ ID ] and not ply.ZoneExtra then
4832 ply.ZoneExtra = true
4833 params[ #params + 1 ] = "Additional"
4834 elseif ply.ZoneExtra then
4835 params[ #params + 1 ] = "Additional"
4836 end
4837
4838 -- Check if it's embeddable
4839 if self.Embedded[ ID ] then
4840 params[ #params + 1 ] = "Embedded (" .. (ply.AdminZoneID and ply.AdminZoneID or "None") .. ")"
4841 end
4842
4843 -- Remove blank embed ID
4844 if #params > 1 then
4845 table.remove( params, 1 )
4846 end
4847
4848 -- Set the active session
4849 self.List[ ply ] = {
4850 Active = true,
4851 Start = ply:GetPos(),
4852 Type = ID
4853 }
4854
4855 -- Let the client know we're setting a zone
4856 Core.Send( ply, "Admin", { "EditZone", self.List[ ply ] } )
4857 Core.Print( ply, "Admin", Core.Text( "ZoneStart", Zones.GetName( ID ), string.Implode( ", ", params ) ) )
4858end
4859
4860--[[
4861 Description: Checks if we're setting something and finishes it if we're all good
4862--]]
4863function Zones.Editor:CheckSet( ply, finish, extra )
4864 -- Only finish if we have an active session
4865 if self.List[ ply ] then
4866 -- When we're finishing, actually set the zone
4867 if finish then
4868 if extra then
4869 ply.ZoneExtra = nil
4870 end
4871
4872 -- Finalize the session
4873 self:FinishSet( ply, extra )
4874 end
4875
4876 return true
4877 end
4878end
4879
4880--[[
4881 Description: Cancels a zone placement session
4882--]]
4883function Zones.Editor:CancelSet( ply, force )
4884 -- Clean the list if it exists
4885 if self.List[ ply ] then
4886 Core.CleanTable( self.List[ ply ] )
4887 end
4888
4889 -- Clear session and let the client know of this as well
4890 self.List[ ply ] = nil
4891 Core.Send( ply, "Admin", { "EditZone", self.List[ ply ] } )
4892 Core.Print( ply, "Admin", Core.Text( force and "ZoneCancel" or "ZoneFinish" ) )
4893end
4894
4895--[[
4896 Description: Finishes the session and inserts the new entry straight into the database
4897--]]
4898function Zones.Editor:FinishSet( ply, extra )
4899 -- Get the active editor
4900 local editor = self.List[ ply ]
4901 if not editor then return end
4902
4903 -- Custom zones
4904 if ply.AdminZoneID and Zones.Editor.EmbeddedOffsets[ editor.Type ] then
4905 local embed = editor.Type + Zones.Editor.EmbeddedOffsets[ editor.Type ] + ply.AdminZoneID
4906
4907 if editor.Type == Zones.Type["Stage End"] then
4908 ply.AdminZoneID = ply.AdminZoneID + 1
4909 Core.Print( ply, "Admin", Core.Text( "ZoneIDIncrement", ply.AdminZoneID ) )
4910 end
4911
4912 editor.Type = embed
4913 end
4914
4915 -- If we haven't got an end set yet, set it to the current position
4916 if not editor.End then
4917 editor.End = ply:GetPos()
4918 end
4919
4920 -- Obtain the coordinates
4921 local s, e = editor.Start, editor.End
4922 local Min = util.TypeToString( Vector( math.min( s.x, e.x ), math.min( s.y, e.y ), math.min( s.z, e.z ) ) )
4923 local Max = util.TypeToString( Vector( math.max( s.x, e.x ), math.max( s.y, e.y ), math.max( s.z + 128, e.z + 128 ) ) )
4924
4925 -- Check if it's a new zone or an existing one and update it
4926 Prepare(
4927 "SELECT nType FROM game_zones WHERE szMap = {0} AND nType = {1}",
4928 { Timer:GetMap(), editor.Type }
4929 )( function( data, varArg, szError )
4930 if Core.Assert( data, "nType" ) and not varArg then
4931 Prepare( "UPDATE game_zones SET vPos1 = {0}, vPos2 = {1} WHERE szMap = {2} AND nType = {3}", { Min, Max, Timer:GetMap(), editor.Type } )
4932 else
4933 Prepare( "INSERT INTO game_zones VALUES ({0}, {1}, {2}, {3})", { Timer:GetMap(), editor.Type, Min, Max } )
4934 end
4935 end, extra )
4936
4937 -- Close the session and reload all zones
4938 self:CancelSet( ply )
4939 Zones.Reload()
4940end
4941
4942
4943--[[
4944 Description: Setups up all server entities
4945 Used by: Map initialization
4946--]]
4947local MapPlatforms, PlatformBoosters = {}, {}
4948function Core.SetupMapEntities()
4949 if Zones.IsSetup then return end
4950
4951 -- Make sure it doesn't run twice
4952 Zones.IsSetup = true
4953
4954 -- Load entities
4955 Zones.Setup()
4956
4957 -- Execute map checks
4958 Zones.CheckOptions()
4959
4960 -- Check the spawns
4961 Zones.SetSpawnAngles()
4962
4963 -- Clean the table if there is anything in it
4964 if not MapPlatforms.NoWipe then
4965 Core.CleanTable( MapPlatforms )
4966 Core.CleanTable( PlatformBoosters )
4967 else
4968 MapPlatforms.NoWipe = nil
4969 end
4970
4971 -- Remove extra pointless stuff that lags
4972 hook.Remove( "PlayerTick", "TickWidgets" )
4973 hook.Remove( "PreDrawHalos", "PropertiesHover" )
4974
4975 -- Surfers hate bullets!
4976 if Config.IsSurf then
4977 hook.Remove( "PlayerPostThink", "ProcessFire" )
4978 end
4979
4980 -- Check if we have additional functions to be executed
4981 if Core.BonusEntitySetup then
4982 Core.BonusEntitySetup()
4983 end
4984
4985 -- Check if we have some custom PostInit hooks
4986 if Zones.CustomEntitySetup then
4987 Zones.CustomEntitySetup( Timer )
4988 end
4989
4990 -- Pre-cache models
4991 for _,model in pairs( Core.ContentText( "ValidModels" ) ) do util.PrecacheModel( "models/player/" .. model .. ".mdl" ) end
4992 for _,model in pairs( Core.ContentText( "FemaleModels" ) ) do util.PrecacheModel( "models/player/" .. model .. ".mdl" ) end
4993
4994 -- Enable fading platforms
4995 for _,ent in pairs( ents.FindByClass( "func_lod" ) ) do
4996 ent:SetRenderMode( RENDERMODE_TRANSALPHA )
4997 end
4998
4999 -- Gets rid of the "Couldn't dispatch user message (21)" errors in console
5000 for _,ent in pairs( ents.FindByClass( "env_hudhint" ) ) do
5001 ent:Remove()
5002 end
5003
5004 -- Enable fading non-platforms
5005 for _,ent in pairs( ents.GetAll() ) do
5006 if ent:GetRenderFX() != 0 and ent:GetRenderMode() == 0 then
5007 ent:SetRenderMode( RENDERMODE_TRANSALPHA )
5008 end
5009 end
5010
5011 -- Since this might get called a lot, localize
5012 local index = IndexPlatform
5013 local inbox = ents.FindInBox
5014 local inmap = Timer:GetMap()
5015
5016 -- Loop over all door platforms
5017 for _,ent in pairs( ents.FindByClass( "func_door" ) ) do
5018 if not ent.IsP then continue end
5019
5020 local mins = ent:OBBMins()
5021 local maxs = ent:OBBMaxs()
5022 local h = maxs.z - mins.z
5023
5024 if (h > 80 and not Zones.SpecialDoorMaps[ inmap ]) or Zones.MovingDoorMaps[ inmap ] then continue end
5025 local tab = inbox( ent:LocalToWorld( mins ) - Vector( 0, 0, 10 ), ent:LocalToWorld( maxs ) + Vector( 0, 0, 5 ) )
5026
5027 if (tab and #tab > 0) or ent.BHSp > 100 then
5028 local teleport
5029 for i = 1, #tab do
5030 if IsValid( tab[ i ] ) and tab[ i ]:GetClass() == "trigger_teleport" then
5031 teleport = tab[ i ]
5032 end
5033 end
5034
5035 if teleport or ent.BHSp > 100 then
5036 ent:Fire( "Lock" )
5037 ent:SetKeyValue( "spawnflags", "1024" )
5038 ent:SetKeyValue( "speed", "0" )
5039 ent:SetRenderMode( RENDERMODE_TRANSALPHA )
5040
5041 if ent.BHS then
5042 ent:SetKeyValue( "locked_sound", ent.BHS )
5043 else
5044 ent:SetKeyValue( "locked_sound", "DoorSound.DefaultMove" )
5045 end
5046
5047 local nid = ent:EntIndex()
5048 index( nid )
5049 MapPlatforms[ #MapPlatforms + 1 ] = nid
5050
5051 if ent.BHSp > 100 then
5052 index( nid, ent.BHSp )
5053 PlatformBoosters[ nid ] = ent.BHSp
5054 end
5055 end
5056 end
5057 end
5058
5059 -- Loop over all button platforms
5060 for _,ent in pairs( ents.FindByClass( "func_button" ) ) do
5061 if not ent.IsP then continue end
5062 if ent.SpawnFlags == "256" then
5063 local mins = ent:OBBMins()
5064 local maxs = ent:OBBMaxs()
5065 local tab = inbox( ent:LocalToWorld( mins ) - Vector( 0, 0, 10 ), ent:LocalToWorld( maxs ) + Vector( 0, 0, 5 ) )
5066
5067 if tab and #tab > 0 then
5068 local teleport
5069 for i = 1, #tab do
5070 if IsValid( tab[ i ] ) and tab[ i ]:GetClass() == "trigger_teleport" then
5071 teleport = tab[ i ]
5072 end
5073 end
5074
5075 if teleport then
5076 ent:Fire( "Lock" )
5077 ent:SetKeyValue( "spawnflags", "257" )
5078 ent:SetKeyValue( "speed", "0" )
5079 ent:SetRenderMode( RENDERMODE_TRANSALPHA )
5080
5081 if ent.BHS then
5082 ent:SetKeyValue( "locked_sound", ent.BHS )
5083 else
5084 ent:SetKeyValue( "locked_sound", "None (Silent)" )
5085 end
5086
5087 local nid = ent:EntIndex()
5088 index( nid )
5089 MapPlatforms[ #MapPlatforms + 1 ] = nid
5090 end
5091 end
5092 end
5093 end
5094end
5095
5096--[[
5097 Description: Sends the platform indexes as well as timer indexes to the client
5098--]]
5099function Zones.SendPlatforms( ply )
5100 -- Send entity data
5101 NetPrepare( "Client/Entities", { ClientEnts, Zones.Type, MapPlatforms, PlatformBoosters } ):Send( ply )
5102end
5103Core.Register( "Global/Platforms", Zones.SendPlatforms )
5104
5105--[[
5106 Description: Broadcast all timer entities
5107 Used by: Zone reloading
5108--]]
5109function Zones.BroadcastClientEnts()
5110 NetPrepare( "Client/Entities", { ClientEnts } ):Broadcast()
5111end
5112
5113
5114
5115-- Getters and setters
5116local Lefty, Righty, Bypass, PsuedoOff = IN_LEFT, IN_RIGHT
5117
5118--[[
5119 Description: Formats the date
5120 Notes: I moved it here because I thought the format looked messy
5121--]]
5122function Timer.GetCurrentDate( bFormat )
5123 if bFormat then
5124 return OD( "%Y-%m-%d %H:%M:%S", OT() )
5125 else
5126 return OT()
5127 end
5128end
5129
5130--[[
5131 Description: Returns the current map
5132 Notes: No idea why I made a function for caching this (probably because game.GetMap() didn't fit with the rest)
5133--]]
5134function Timer:GetMap()
5135 if not self.CurrentMap then
5136 self.CurrentMap = game.GetMap()
5137 end
5138
5139 return self.CurrentMap
5140end
5141
5142--[[
5143 Description: Converts seconds to a readable and detailed time
5144--]]
5145function Core.ConvertTime( Seconds )
5146 if Seconds >= 3600 then
5147 return FO( "%d:%.2d:%.2d.%.3d", FL( Seconds / 3600 ), FL( Seconds / 60 % 60 ), FL( Seconds % 60 ), FL( Seconds * 1000 % 1000 ) )
5148 else
5149 return FO( "%.2d:%.2d.%.3d", FL( Seconds / 60 % 60 ), FL( Seconds % 60 ), FL( Seconds * 1000 % 1000 ) )
5150 end
5151end
5152Timer.Convert = Core.ConvertTime
5153
5154--[[
5155 Description: Returns a variable from the Timer instance
5156 Used by: Command, admin
5157--]]
5158function Core.GetMapVariable( szType )
5159 if szType == "Plays" then
5160 return Timer.Plays
5161 elseif szType == "Multiplier" then
5162 return Timer.Multiplier
5163 elseif szType == "Bonus" then
5164 return Timer.BonusMultiplier
5165 elseif szType == "Options" then
5166 return Timer.Options
5167 elseif szType == "OptionList" then
5168 return Zones.Options
5169 elseif szType == "Tier" then
5170 return Timer.Tier
5171 elseif szType == "Type" then
5172 return Timer.Type
5173 elseif szType == "IsBindBypass" then
5174 return Bypass
5175 elseif szType == "Platforms" then
5176 return MapPlatforms
5177 end
5178end
5179
5180--[[
5181 Description: Sets a variable on the timer object
5182 Used by: Admin panel
5183--]]
5184function Core.SetMapVariable( szType, varObj )
5185 Timer[ szType ] = varObj
5186end
5187
5188--[[
5189 Description: Allows remote files to disable +left and +right checking
5190 Used by: Map files for fly maps
5191--]]
5192function Core.BypassStrafeBinds( bValue )
5193 Bypass = bValue
5194end
5195
5196--[[
5197 Description: Returns a random number in range
5198 Notes: Apparently the first few calls to math.random are not exactly random (caused for weird behavior in RandomPairs)
5199--]]
5200function Core.TrueRandom( nUp, nDown )
5201 if not PsuedoOff then
5202 MR() MR() MR()
5203 PsuedoOff = true
5204 end
5205
5206 return MR( nUp, nDown )
5207end
5208RTV.TrueRandom = Core.TrueRandom
5209
5210--[[
5211 Description: Gets all the zone entities from the table
5212 Used by: Admin panel to show what zones to remove
5213--]]
5214function Core.GetZoneEntities( data, set )
5215 if data then
5216 if set then
5217 ZoneCache = set
5218 else
5219 return ZoneCache
5220 end
5221 else
5222 return ZoneEnts
5223 end
5224end
5225
5226--[[
5227 Description: Translates a zone name to a zone ID
5228--]]
5229function Core.GetZoneID( szType )
5230 if not szType then return Zones.Type end
5231 return Zones.Type[ szType ]
5232end
5233
5234--[[
5235 Description: Gets the center point of a zone with the given type
5236 Used by: Command (end and bend)
5237--]]
5238function Core.GetZoneCenter( bonus, other, embed )
5239 return Zones.GetCenterPoint( other and Zones.Type[ other ] or (bonus and Zones.Type["Bonus End"] or Zones.Type["Normal End"]), embed )
5240end
5241
5242--[[
5243 Description: Returns more data about a zone
5244--]]
5245function Core.GetZoneInfo( zone )
5246 return Zones:GetZoneInfo( zone )
5247end
5248
5249--[[
5250 Description: Returns the zone editor table for remote usage
5251 Used by: Admin panel
5252--]]
5253function Core.GetZoneEditor()
5254 return Zones.Editor
5255end
5256
5257--[[
5258 Description: Reloads options and executes checks
5259--]]
5260function Core.ReloadMapOptions()
5261 Zones.CheckOptions()
5262end
5263
5264--[[
5265 Description: Checks if an option is applied
5266--]]
5267function Core.IsMapOption( opt )
5268 return Zones.IsOption( opt )
5269end
5270
5271--[[
5272 Description: Gets all bonus IDs
5273--]]
5274function Core.GetBonusIDs()
5275 return Zones.GetBonusIDs()
5276end
5277
5278--[[
5279 Description: Gets the multiplier for the given style
5280--]]
5281function Core.GetMultiplier( nStyle, bAll )
5282 return Timer:GetMultiplier( nStyle, bAll )
5283end
5284
5285--[[
5286 Description: Gets the average for the given style
5287--]]
5288function Core.GetAverage( nStyle )
5289 if GetAverage( nStyle ) > 0 then
5290 CalcAverage( nStyle )
5291 return GetAverage( nStyle )
5292 else
5293 return 0
5294 end
5295end
5296
5297--[[
5298 Description: Adds a version to the RTV tracker
5299--]]
5300function Core.AddMaplistVersion( nAmount )
5301 RTV:UpdateVersion( nAmount )
5302end
5303
5304--[[
5305 Description: Gets the map list version (duh, that's what the name of the function implies)
5306--]]
5307function Core.GetMaplistVersion()
5308 return RTV.Version
5309end
5310
5311--[[
5312 Description: Executes a type of map check and returns that data
5313--]]
5314function Core.MapCheck( szMap, IsBSP, GetData )
5315 if IsBSP then
5316 return RTV.IsAvailable( szMap )
5317 else
5318 if GetData then
5319 return RTV.GetMapData( szMap )
5320 else
5321 return RTV.MapExists( szMap )
5322 end
5323 end
5324end
5325
5326--[[
5327 Description: Change whetehr or not the map vote is being cancelled
5328 Used by: Admin panel
5329--]]
5330function Core.ChangeVoteCancel()
5331 RTV.CancelVote = not RTV.CancelVote
5332
5333 return RTV.CancelVote
5334end
5335
5336--[[
5337 Description: Gets all the records in the top list cache
5338--]]
5339function Core.GetPlayerTop( nStyle )
5340 return TopListCache[ nStyle ] or {}
5341end
5342
5343--[[
5344 Description: Gets all the players holding WRs
5345--]]
5346function Core.GetPlayerWRTop( nStyle )
5347 return Timer.TopWRList[ nStyle ] or {}
5348end
5349
5350--[[
5351 Description: Make sure that records can be inserted for this style
5352--]]
5353function Core.EnsureStyleRecords( nStyle )
5354 if not Records[ nStyle ] then
5355 Records[ nStyle ] = {}
5356 end
5357end
5358
5359--[[
5360 Description: Gets all records on a player for each style
5361--]]
5362function Core.GetStyleRecords( ply )
5363 local values = {}
5364 local styles = Core.GetStyles()
5365 for i = Styles.Normal, Config.MaxStyle do
5366 if not styles[i] then continue end
5367 local nTime, nID = Timer.GetPlayerRecord( ply, i )
5368 if nTime > 0 and nID > 0 then
5369 values[ i ] = { nTime, nID }
5370 end
5371 end
5372
5373 return values
5374end
5375
5376--[[
5377 Description: Gets a part of the record list (from nStart to nMaximum)
5378 Used by: Commands, network
5379--]]
5380function Core.GetRecordList( nStyle, nStart, nMaximum )
5381 local tab = {}
5382
5383 for i = nStart, nMaximum do
5384 if Records[ nStyle ] and Records[ nStyle ][ i ] then
5385 tab[ i ] = Records[ nStyle ][ i ]
5386 end
5387 end
5388
5389 return tab
5390end
5391
5392--[[
5393 Description: Gets the top times in a list
5394 Used by: Commands
5395--]]
5396function Core.GetTopTimes()
5397 local tab = {}
5398
5399 for style,data in pairs( Records ) do
5400 if data[ 1 ] and data[ 1 ]["nTime"] then
5401 tab[ style ] = data[ 1 ]
5402 end
5403 end
5404
5405 return tab
5406end
5407
5408--[[
5409 Description: Gets the amount of records on a style
5410--]]
5411function Core.GetRecordCount( nStyle )
5412 return GetRecordCount( nStyle )
5413end
5414
5415--[[
5416 Description: Gets the base statistics loaded on startup
5417 Used by: F1 Help window
5418--]]
5419function Core.GetBaseStatistics()
5420 return Timer.BaseStatistics
5421end
5422
5423--[[
5424 Description: Returns when a map was last played
5425--]]
5426function Core.GetLastPlayed( szMap )
5427 if Maps[ szMap ] then
5428 return Maps[ szMap ].szDate, Maps[ szMap ]
5429 end
5430end
5431
5432--[[
5433 Description: Clears out the RTV wait period
5434 Used by: Server command
5435--]]
5436function Core.ClearWaitPeriod()
5437 RTV.WaitPeriod = 0
5438end
5439
5440--[[
5441 Description: Returns the amount of time left before the vote starts
5442 Used by: Sockets
5443--]]
5444function Core.GetTimeLeft()
5445 return RTV.End - ST()
5446end
5447
5448--[[
5449 Description: Updates the command count statistic
5450 Used by: Post entity init
5451--]]
5452function Core.UpdateCommandCount()
5453 Timer.BaseStatistics[ 5 ], Timer.BaseStatistics[ 6 ] = Core.CountCommands()
5454end
5455
5456
5457
5458-- Fixes short lags upon loadout of several maps
5459local function KeyValueChecks( ent, key, value )
5460 if ent:GetClass() == "info_player_counterterrorist" or ent:GetClass() == "info_player_terrorist" then
5461 if key == "angles" then
5462 if not Timer.Spawns[ value ] then
5463 Timer.Spawns[ value ] = 1
5464 else
5465 Timer.Spawns[ value ] = Timer.Spawns[ value ] + 1
5466 end
5467 end
5468 elseif ent:GetClass() == "info_teleport_destination" then
5469 if key == "origin" then
5470 Timer.Teleports[ #Timer.Teleports + 1 ] = value
5471 elseif key == "angles" then
5472 Timer.Teleports[ #Timer.Teleports ] = { Timer.Teleports[ #Timer.Teleports ], value }
5473 end
5474 elseif ent:GetClass() == "game_player_equip" then
5475 if SU( key, 1, 4 ) == "ammo" or SU( key, 1, 5 ) == "weapon" or SU( 1, 5 ) == "item_" then
5476 return "1"
5477 end
5478 end
5479end
5480hook.Add( "EntityKeyValue", "KeyValueChecks", KeyValueChecks )
5481
5482local uk, uj, us, es = IN_ATTACK2, IN_JUMP, Styles.Unreal, Styles.Extreme
5483local function UnrealBoostKey( ply, key )
5484 if key == uk then
5485 if ply.Style == us then
5486 ply:DoUnrealBoost( nil, nil, 5 )
5487 elseif ply.Style == es then
5488 ply:DoUnrealBoost( 0, nil, 5, ply:GetVelocity() * Vector( 2, 2, 4.5 ) )
5489 end
5490 elseif key == uj and Config.IsSurf then
5491 timer.Simple( 0.01, function()
5492 if not ply:IsOnGround() and ply:GetVelocity().z > 0 then
5493 ply.LastJumped = ST()
5494 end
5495 end )
5496 end
5497end
5498hook.Add( "KeyPress", "UnrealKeyPress", UnrealBoostKey )
5499
5500local function UnrealKeyHold( ply, mv )
5501 if ply:IsOnGround() then return end
5502
5503 local lastBoost = 0
5504 if ply.lastUnrealBoost != nil then
5505 lastBoost = ply.lastUnrealBoost
5506 else
5507 ply.lastUnrealBoost = lastBoost
5508 end
5509
5510 if mv:KeyDown(uk) and ( ply.Style == Styles["Crazy"] or ply.Style == Styles["Cancer"] ) then
5511 if CurTime() - ply.lastUnrealBoost > 0.1 then
5512 local mult = 2.2
5513 ply:SetVelocity( ply:GetVelocity() * Vector( mult, mult, mult * 50 ) )
5514 ply.lastUnrealBoost = CurTime()
5515 end
5516 end
5517end
5518hook.Add( "Move", "UnrealKeyHold", UnrealKeyHold )
5519
5520local function UnrealBoostBind( ply, _, varArgs )
5521 if ply.Style == us then
5522 local force
5523 if varArgs and varArgs[ 1 ] and tonumber( varArgs[ 1 ] ) then
5524 force = tonumber( varArgs[ 1 ] )
5525
5526 if force then
5527 force = math.floor( force )
5528
5529 if force < 1 or force > 4 then
5530 force = 1
5531 end
5532 end
5533 end
5534
5535 ply:DoUnrealBoost( force )
5536 end
5537end
5538concommand.Add( "unrealboost", UnrealBoostBind )
5539
5540-- Gamemode specific checks
5541if not Config.IsSurf then
5542 -- Check for this press every frame, otherwise we'll have to do it on the client and I don't like that
5543 local function BlockLeftRight( ply, data )
5544 -- Make sure to only check it when we set it to block it (so we can still use it on fly maps)
5545 if Bypass then return end
5546
5547 -- Whenever we are pressing +left or +right, check if they have any timers, and stop them if they do
5548-- if data:KeyDown( Righty ) then
5549-- if ply.TAS then return end
5550-- if ply.Tn or ply.Tb then
5551-- if ply:StopAnyTimer() then
5552-- Core.Print( ply, "Timer", Core.Text( "StyleLeftRight" ) )
5553-- end
5554-- end
5555-- end
5556 end
5557 hook.Add( "SetupMove", "BlockLeftRight", BlockLeftRight )
5558elseif Config.IsSurf then
5559 -- Check for each jump landing to make sure we're legit
5560 local LastMessaged = {}
5561 local function PrehopLimitation( ply )
5562 if ply.Practice or Zones.IsOption( Zones.Options.TelehopMap ) then return end
5563 if ply.InSpawn and ply:GetVelocity():Length2D() != 0 then
5564 if ply.LastJumped and ST() - ply.LastJumped > 1 then return end
5565
5566 ply:ResetSpawnPosition()
5567
5568 timer.Simple( 0.01, function()
5569 if IsValid( ply ) then
5570 ply:SetLocalVelocity( Vector( 0, 0, 0 ) )
5571 end
5572 end )
5573
5574 if not LastMessaged[ ply ] or ST() - LastMessaged[ ply ] > 2 then
5575 Player.Notification( ply, "Popup", { "Timer", Core.Text( "ZoneJumpInside" ), "information", 4 } )
5576 LastMessaged[ ply ] = ST()
5577 end
5578 end
5579 end
5580 hook.Add( "OnPlayerHitGround", "PrehopLimitation", PrehopLimitation )
5581end
5582
5583-- Load all extensions
5584if file.Exists( Config.BaseType .. "/gamemode/extensions", "LUA" ) then
5585 -- Scan the directory for extensions
5586 local files = file.Find( Config.BaseType .. "/gamemode/extensions/*.lua", "LUA" )
5587
5588 -- Create an init function holder
5589 Timer.PostInitFunc = {}
5590
5591 -- Loop over the files
5592 for _,f in pairs( files ) do
5593 if SU( f, 1, 2 ) == "cl" then
5594 AddCSLuaFile( Config.BaseType .. "/gamemode/extensions/" .. f )
5595 elseif SU( f, 1, 2 ) == "sv" then
5596 include( Config.BaseType .. "/gamemode/extensions/" .. f )
5597
5598 if Core.PostInitFunc then
5599 Timer.PostInitFunc[ #Timer.PostInitFunc + 1 ] = Core.PostInitFunc
5600 end
5601
5602 Core.PostInitFunc = nil
5603 end
5604 end
5605end
5606
5607-- Check if we have a map lua file, if we do, execute it
5608local files = file.Find( Config.BaseType .. "/gamemode/maps/*.lua", "LUA" )
5609for _,f in pairs( files ) do
5610 -- Replace for global types
5611 local ef = f:gsub( "wildcard", "*" ):gsub( ".lua", "" )
5612
5613 -- Check if the map matches
5614 if (string.find( ef, "*", 1, true ) and string.match( Timer:GetMap(), ef )) or f:gsub( ".lua", "" ) == Timer:GetMap() then
5615 -- Check overrides
5616 if Zones[ "NoWildcard" ] and Zones[ "NoWildcard" ][ Timer:GetMap() ] and string.find( f, "wildcard", 1, true ) then continue end
5617
5618 -- Create a global table to be populated
5619 __HOOK = {}
5620 __MAP = {}
5621
5622 -- Load the individual map file
5623 include( Config.BaseType .. "/gamemode/maps/" .. f )
5624
5625 -- Set the hook counter
5626 Timer.HookCount = (Timer.HookCount or 0) + 1
5627
5628 -- Add all the custom hooks
5629 for identifier,func in pairs( __HOOK ) do
5630 hook.Add( identifier, identifier .. "_" .. Timer:GetMap() .. "_" .. Timer.HookCount, func )
5631 end
5632
5633 -- Allow custom entities
5634 for identifier,bool in pairs( __MAP ) do
5635 if not Zones[ identifier ] then
5636 Zones[ identifier ] = {}
5637
5638 if identifier == "CustomEntitySetup" then
5639 Zones[ identifier ] = bool
5640 break
5641 end
5642 end
5643
5644 Zones[ identifier ][ Timer:GetMap() ] = bool
5645 end
5646
5647 -- Dispose of that filthy global
5648 __HOOK = nil
5649 __MAP = nil
5650 end
5651end
5652
5653local sounds = file.Find( Config.BaseType .. "/content/sound/" .. Config.MaterialID .. "/*.mp3", "LUA" )
5654for _,f in pairs( sounds ) do
5655 WRSounds[ #WRSounds + 1 ] = string.sub( f, 1, #f - 4 )
5656end