Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ####################################################
- # Copyright 2024 LAMA (https://x.com/lama_creator)
- # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- ####################################################
- # VERSION: 24_02b
- ####################################################
- ##################### README #######################
- ####################################################
- # DISCLAIMERS:
- # - Those are only things I discovered by myself by doing some little testing, so I might be wrong about some things
- # - Use at your own risk, as every Verse code, this code could stop working if Epic makes changes on fortnite devices that rely on it or on the crash behaviour itself
- #
- # IMPORTANT:
- # The printed/reported crashed device isn't necessarily the device that contains the faulty code (read more below)
- #
- # HOW IT WORKS:
- # When a crash occurs, the whole stack trace that caused the crash is aborted
- # (thus the top most caller device of the stack trace will be the one to crash)
- #
- # Since the top most calling function of all devices is (almost?) always an OnBegin function,
- # the crashed device will crash everything that's been called inside its own OnBegin (even spawned coroutines)
- # but the crashed device functions can still be called
- #
- # NOTES:
- # - Has been tested on 100M+ players maps
- # - Doesn't detect server crashes (Epic ones)
- # - Doesn't detect infinite loops
- # - The per device CrashAnalytics field is not required (will send a global crash report instead)
- # - A 'non crashable' device can trigger a crash on a crashable device (if it's the crashable device that called the faulty code)
- ####################################################
- ####################################################
- ###################### TODO ########################
- ####################################################
- # Add an in game UI for whenever a crash is detected (so that players can also report it and add context to it)
- # Allow for more granular crash detection
- # Find a way to find the device that contains the faulty code?
- # Find a way to restart the crashed device?
- # Allow script to dynamically register/unregister devices that gets loaded/unloaded from data layers
- using { /Fortnite.com/Devices }
- using { /Verse.org/Simulation }
- using { /Verse.org/Random }
- using { /Verse.org/Colors }
- # Allow for printing an error without crashing the whole device
- PrintError(String: string):void = Print("Error: {String}", ?Color := NamedColors.Red)
- PrintOutputLog(String: string):void = Print(String, ?Duration := 0.0)
- var RegisteredCrashableDevices : weak_map(session, [crashable_device]crashable_device) = map{}
- RegisterCrashableDevice(Device: crashable_device):void=
- Session := GetSession()
- if(not RegisteredCrashableDevices[Session]):
- Print("Registering crashable devices, see Output Log for the detailed list")
- option{set RegisteredCrashableDevices[Session] = map{}}
- if(not RegisteredCrashableDevices[Session][Device]):
- PrintOutputLog("Successfully registered crashable device '{Device}' (CAN_RESTART_ROUND={Device.RestartRoundOnCrash? and "YES" or "NO"})")
- option{set RegisteredCrashableDevices[Session][Device] = Device}
- else:
- PrintError("Tried to register crashable device '{Device}' twice")
- crashable_device := class<unique>(creative_device):
- @editable
- DeviceName : string = ""
- @editable
- var<private> RestartRoundOnCrash : logic = true
- # Naming convention for the "Event Name" is crash_{device_name}
- @editable
- CrashAnalytics : ?analytics_device = false
- var<private> IsAlive : logic = true
- PingEvent<private> : event() = event(){}
- CrashEvent<private> : event() = event(){}
- # Any (3568) error here means you need to rename all your (crashable_device).OnBegin() functions by OnCrashableDeviceBegin()
- OnBegin<override><final>()<suspends>:void=
- # This suspending thread has to be spawned by Self from Self in order for it to crash when something else crashes inside the device
- spawn{WatchPingEvents()}
- spawn{AwaitManualCrashTest()}
- # Register before overridden OnBegin so we don't have to deal with OnBegin methods that never finish
- RegisterCrashableDevice(Self)
- OnCrashableDeviceBegin()
- OnCrashableDeviceBegin<public>()<suspends>:void={}
- # This thread will crash if Verse crashes
- WatchPingEvents<private>()<suspends>:void=
- loop:
- PingEvent.Await()
- set IsAlive = true
- # needs no_rollback :'(
- Ping<public>():logic=
- set IsAlive = false
- PingEvent.Signal()
- IsAlive
- Crash(?AllowRoundToRestart : logic = true):void=
- if(not AllowRoundToRestart?):
- set RestartRoundOnCrash = false
- CrashEvent.Signal()
- AwaitManualCrashTest<private>()<suspends>:void=
- CrashEvent.Await()
- Sleep(0.0) # If we don't do so, the detected crashed device will be the caller of the Crash() method
- Err("!!! CrashTest !!!")
- # OVERRIDE ME: If you don't want to use the DeviceName field because you already have a way to dynamically generate a name
- GetDeviceName()<transacts><decides>:string = (__:?string=false)?
- ToString(CrashableDevice: crashable_device)<transacts>:string =
- FoundName := CrashableDevice.GetDeviceName[] or CrashableDevice.DeviceName
- FoundName.Length > 0 and FoundName or "UNKNOWN"
- verse_crash_detector := class(creative_device):
- @editable
- Timer : timer_device = timer_device{}
- @editable
- CanRestartRoundOnCrash : logic = true
- @editable
- GlobalCrashAnalytics : analytics_device = analytics_device{}
- @editable
- CRASH_BUTTON : ?button_device = false
- OnBegin<override>()<suspends>:void=
- Timer.Disable() # Set default behaviour to "safe mode" (do not end round)
- # Listen to registered devices crashes
- spawn{WatchCrashes()}
- if(CrashButton := CRASH_BUTTON?):
- loop:
- CrashButton.InteractedWithEvent.Await()
- if(RandomDevice := Shuffle(GetAliveDevices())[0]):
- RandomDevice.Crash() # Triggers a simulated internal Verse crash
- GetAliveDevices()<transacts>:[]crashable_device =
- for(Device : RegisteredCrashableDevices[GetSession()], Device.IsAlive?) { Device }
- WatchCrashes<private>()<suspends>:void=
- # Allow for round to restart when a crash occurs
- Timer.Enable()
- Timer.Start()
- # Constantly look for any crashed device
- loop:
- for(Device : GetAliveDevices()):
- Pong := Device.Ping()
- # Crash detected
- if(not Pong?):
- # Send analytics data if analytics is plugged
- if(Analytics := Device.CrashAnalytics?, AnyAgent := GetPlayspace().GetPlayers()[0]):
- Analytics.Submit(AnyAgent)
- PrintError("verse_crash_detector: Device '{Device}' called a function that caused a crash")
- # Only restart round if it's needed
- if(CanRestartRoundOnCrash?, Device.RestartRoundOnCrash?):
- # Let the timer end -> trigger HUD -> send global crash analytics -> restart round
- break
- else:
- # Send global crash analytics if device crash analytics is not set
- if(not Device.CrashAnalytics?, AnyAgent := GetPlayspace().GetPlayers()[0]):
- GlobalCrashAnalytics.Submit(AnyAgent)
- Timer.Reset()
- Timer.Start()
- Sleep(2.0)
Advertisement
Add Comment
Please, Sign In to add comment