-
Notifications
You must be signed in to change notification settings - Fork 6
Introduce serial and parallel caster modes with SoA simulation architecture #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
weenachuangkud
wants to merge
129
commits into
main
Choose a base branch
from
major
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
129 commits
Select commit
Hold shift + click to select a range
1d371ac
Add .newParallel and .new() feature
weenachuangkud 945b5f8
Remove FastCastParallel.new and add docs to FastCast.new/newParallel
weenachuangkud 14d9753
Update TODO.md
weenachuangkud 8c01cc5
feat: Add ActiveCastSerial for main thread simulation
weenachuangkud 3774b17
feat: Add BaseCastSerial for serial caster implementation
weenachuangkud 2acf367
feat: Add FastCastSerial methods and remove FastCastEventsModule from…
weenachuangkud 87158c9
feat: Add SerialSimulation with SoA pattern and single RunService
weenachuangkud 51a5f24
refactor: ActiveCastSerial uses SerialSimulation
weenachuangkud 4f6340b
revert: Remove conflicting SerialSimulation reference from ActiveCast
weenachuangkud 11c458c
refactor: Serial uses SoA pattern with SerialSimulation
weenachuangkud 13d8f71
refactor: Parallel mode uses SoA with ParallelSimulation
weenachuangkud 2f423e5
refactor: Remove UpdateConnection and metatable from ActiveCast
weenachuangkud 4d7b209
perf: Remove xpcall/pcall from hot path for performance
weenachuangkud 4b45156
Update TODO.md
weenachuangkud 0bb5986
refactor: Change Trajectories to Trajectory (single object, not array)
weenachuangkud 47d3c21
feat: Add Motor6D transform feature
weenachuangkud 28d3420
feat: Add Motor6D support to Parallel mode
weenachuangkud 55abdb0
fix: Add MovementMethod to ActiveCast for Parallel mode
weenachuangkud 3afbe66
docs: Update TODO.md with completed items
weenachuangkud 3ef6be3
fix: HighFidelityBehavior=2 bug - subRayDir used delta instead of tim…
weenachuangkud b465441
docs: update comments
weenachuangkud 59a1f99
docs: Update TODO.md with all completed items including bug fixes
weenachuangkud 5b751e7
docs: Add comprehensive API documentation following devforum structure
weenachuangkud 87692f1
release: bump version to 0.1.0 with Serial/Parallel modes, Motor6D, SoA
weenachuangkud 82519af
chore: remove version comments from source files
weenachuangkud 0e2fa47
docs: add Rojo installation guide to README
weenachuangkud 5b2dad1
fix: resolve CodeRabbit issues from PR #39
weenachuangkud e816b67
Add return statement to guarding
weenachuangkud b495224
Add return statement to guarding
weenachuangkud ad03254
docs: Changed FastCast/.git to .git
weenachuangkud 51225b1
docs: Change speed to SPEED
weenachuangkud 5fb543f
fix: use Copy instead of reference
weenachuangkud d4c9576
fix: use castData.RayParams
weenachuangkud b9eaf04
resolve coderabbit: Validate acceleration before it is read for kinem…
weenachuangkud 2d7b353
fix: make BaseCastSerial state instance-local to prevent cross-instan…
weenachuangkud ca39f6a
chore: remove unused constants from ActiveCastSerial.luau
weenachuangkud 9a834ae
fix: add missing FastCastEnums require in serial code example
weenachuangkud 7e684d2
fix: replace undefined latestTrajectory with trajectory in SimulateCast
weenachuangkud c0003b2
fix: correct Init params, remove self.self.* patterns, rename BindObj…
weenachuangkud 16fa2cc
fix: check ParallelSimulation.StepConnection instead of SerialSimulat…
weenachuangkud 58c9792
fix: forward BindableEvent to Signals, fix GetVelocityCast, rebase tr…
weenachuangkud cb68225
add: architecture.md
weenachuangkud 4b8c63f
Update architecture.md
weenachuangkud 1b6d8fa
Update architecture.md
weenachuangkud bd45c11
fix: implement HighFidelitySegmentSize and HighFidelityBehavior in So…
weenachuangkud 8bfdad3
feat: implement RayPierce/CanPierce in SoA simulations
weenachuangkud edde9f8
feat: add event config/module gating to SoA simulations
weenachuangkud ea27c67
fix: correct CastFire event signature to match legacy API
weenachuangkud bc53d7a
Comment out unused functions in ActiveCast.lua(legacy code)
weenachuangkud b6fbb3e
remove: spacing
weenachuangkud 60bca8c
feat: add debug logging to SoA simulations matching legacy ActiveCast
weenachuangkud ad907eb
Update typedef
weenachuangkud dbe98f7
Comment out unused function
weenachuangkud c1742b8
Comment out unused function
weenachuangkud d045c0c
feat: add cast visualization matching legacy ActiveCast
weenachuangkud 0df246c
Add .md files
weenachuangkud 029690e
fix: remove legacy code
weenachuangkud 5819065
resolve: coderabbit 106-109
weenachuangkud d1273a2
fix: remove legacy code
weenachuangkud 6bb678e
fix: unneeded local variable
weenachuangkud 458896d
fix: Add .Acceleration
weenachuangkud 4afb6e8
Update init.luau
weenachuangkud 2625a8c
Merge branch 'major' of https://github.com/weenachuangkud/FastCast2 i…
weenachuangkud 78a9cd9
Add AGENTS.md
weenachuangkud 174c3ee
Move src path
weenachuangkud e458c4e
Update wally.tom version
weenachuangkud adcdba1
fix: change date
weenachuangkud 43f5fa0
remove: unnec comment
weenachuangkud 697c32f
No need to use variable
weenachuangkud 3009a15
Use legacy code
weenachuangkud 26a522d
FastCastSerial doesn't use FastCastModule
weenachuangkud 1b446c9
Change event handle to legacy style code
weenachuangkud 0269fc2
Update sourcemap.json
weenachuangkud 7f30f92
Use function instead of signal | function to reduce complexity
weenachuangkud 5402f55
Update init.luau
weenachuangkud b48ee41
Fix type error
weenachuangkud 13b069a
Add some note
weenachuangkud 06b6a77
Reimplement in OOP
weenachuangkud 055127c
fix: types error
weenachuangkud 5767fa3
Use Movement mode for FastCastSerial
weenachuangkud 1d1383a
Simplify to FastCast.newBehavior
weenachuangkud deca9d5
Update init.luau
weenachuangkud aebe66b
Update init.luau
weenachuangkud 7489844
add BaseCast:SetMovementMode
weenachuangkud 33e6f5b
Update ClientVM
weenachuangkud f521d56
Update VMs
weenachuangkud c05f279
Simplify Utility methods
weenachuangkud c713067
fix: FastCastM.TerminateCast
weenachuangkud 1dcde6d
Remove unused if statement
weenachuangkud 97a7a45
Remove DestroySignal from init.luau
weenachuangkud 9322f94
Add src/*.legacy.luau to .gitignore
weenachuangkud 5bb64f8
Remove: AutomaticPerformance
weenachuangkud f6befcb
Update ParallelSimulation
weenachuangkud 284f8fc
Clean up ActiveCastSerial.luau
weenachuangkud 4d3bec4
Replace legacy ActiveCast with new ActiveCast file
weenachuangkud 0b591ea
Change .new to .createCastData
weenachuangkud 5944fc0
Change BaseCast to BaseCastParallel name
weenachuangkud 32c7e25
Update init.luau
weenachuangkud 0c114f5
Add _parallel
weenachuangkud 1f08cfb
Add CanPierce to FastCastEventsConfig
weenachuangkud c441a8b
feat: ObjectCache in each actors instead of 1 host
weenachuangkud a519c34
Update init.luau
weenachuangkud b76e840
Add TODO note
weenachuangkud 4cc00fa
Update init.luau
weenachuangkud cf51c74
Update init.luau
weenachuangkud 9741824
Update BaseCastParallel.luau
weenachuangkud 529765b
Update BaseCastParallel.luau
weenachuangkud 0383d10
Update BaseCastParallel.luau
weenachuangkud 0b65c85
Update sourcemap.json
weenachuangkud 80d5d02
Update BaseCastParallel.luau
weenachuangkud 402018d
Add TODO
weenachuangkud 3f09107
fix: incorect docs comment method
weenachuangkud 6161e33
Use unused parameters
weenachuangkud 3bd6812
Add some boilerplate
weenachuangkud f0edf11
Add some boilerplate
weenachuangkud 191bdc3
Add some boilerplate
weenachuangkud ac8de31
Add some boilerplate
weenachuangkud 1b48f86
Add some boilerplate
weenachuangkud 1304d0d
Add some boilerplate
weenachuangkud 6e647ff
Update ParallelSimulation
weenachuangkud 6599544
Implement ParallelSimulation boilerplate and fix legacy bugs in BaseC…
weenachuangkud c348dab
Refactor QueueEvent to accept varargs and use castID instead of cast …
weenachuangkud 534f79f
Refactor event handling and cosmetic movement to use ParallelSimulati…
weenachuangkud 62c7a9d
Add casts_ID array for efficient iteration in UpdateCasts
weenachuangkud 79a6b65
Update ParallelSimulation.luau to match legacy code
weenachuangkud 22e7a83
Make BaseParallel shares self.Actives with ParallelSimulation
weenachuangkud 0c4ea92
draft: FireQueuedEvents
weenachuangkud 9bfce7a
Add luau language server and .aider to gitignore
weenachuangkud 175939b
Add opencode.json
weenachuangkud File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # AGENTS.md | ||
|
|
||
| ## Project Overview | ||
|
|
||
| FastCast2 is a Roblox projectile library written in Luau, providing high-performance raycasting, blockcasting, and spherecasting with parallel scripting support. It is an unofficial continuation of the original FastCast library. | ||
|
|
||
| - **Language**: Luau (Roblox) | ||
| - **Build Tool**: Rojo (`rojo sync`, `rojo serve`) | ||
| - **Documentation**: Moonwave | ||
| - **Repository**: https://github.com/weenachuangkud/FastCast2 | ||
|
|
||
| ## Development Commands | ||
|
|
||
| - **Sync to Roblox**: `rojo sync -o <place-name>` | ||
| - **Serve live**: `rojo serve` (then connect via Studio → Plugins → Rojo) | ||
| - **Build docs**: `moonwave build` | ||
| - **Publish docs**: `moonwave build --publish` | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| src/FastCast2/ # Main source code (synced to ReplicatedStorage) | ||
| .default.project.json # Rojo project configuration | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| There are no automated tests in this project. Testing is done manually through Roblox Studio. | ||
|
|
||
| ## Code Style | ||
|
|
||
| - Uses Luau static typing | ||
| - Follows standard Luau conventions (PascalCase for types, camelCase for variables) | ||
| - Modules are required via `require(path)` | ||
|
|
||
| ## Important Notes | ||
|
|
||
| - Requires Roblox Studio to run/test code | ||
| - Parallel casting requires `VMsDispatcher` module | ||
| - Cosmetic bullets should have `CanTouch = false`, `CanCollide = false`, `CanQuery = false` | ||
|
|
||
| ## Agent Skills | ||
| To understand specific project workflows, refer to the skills defined here: | ||
| - @skills/architecture.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| -- Services | ||
| local RS = game:GetService("RunService") | ||
| local Rep = game:GetService("ReplicatedStorage") | ||
| local UIS = game:GetService("UserInputService") | ||
| local RepFirst = game:GetService("ReplicatedFirst") | ||
|
|
||
| -- Requires | ||
| local FastCast = require(Rep:WaitForChild("FastCast2")) | ||
|
|
||
| -- Variables | ||
| local ProjectileContainer = Instance.new("Folder") | ||
| ProjectileContainer.Name = "FastCast2PJ_Parallel" | ||
| ProjectileContainer.Parent = workspace | ||
| local ProjectileTemplate = Instance.new("Part") | ||
| ProjectileTemplate.Name = "Projectile" | ||
| ProjectileTemplate.Parent = Rep | ||
| ProjectileTemplate.Size = Vector3.new(1,1,1) | ||
| ProjectileTemplate.CanCollide = false | ||
| ProjectileTemplate.Anchored = true | ||
| ProjectileTemplate.CanQuery = false | ||
| ProjectileTemplate.CanTouch = false | ||
| ProjectileTemplate.Position = Vector3.new(1,1,1) | ||
| ProjectileTemplate.Massless = true | ||
|
|
||
| -- FPS tracking | ||
| local startTime = tick() | ||
| local updateRate = 0.5 | ||
| local fpsTable = {} | ||
| local averageFps = 0 | ||
| local maxFps = 0 | ||
| local minFps = math.huge | ||
| local currentFps = 0 | ||
|
|
||
| RS.Heartbeat:Connect(function(dt: number) | ||
| local fps = 1/dt | ||
| currentFps = fps | ||
| if fps > maxFps then | ||
| maxFps = fps | ||
| end | ||
| if fps < minFps then | ||
| minFps = fps | ||
| end | ||
| table.insert(fpsTable, fps) | ||
|
|
||
| if tick() >= startTime + updateRate then | ||
| local totalFps = 0 | ||
| for _, vFps in fpsTable do | ||
| totalFps += vFps | ||
| end | ||
| averageFps = totalFps / #fpsTable | ||
| fpsTable = {} | ||
| startTime = tick() | ||
| end | ||
| end) | ||
|
|
||
| -- CastParams | ||
| local CastParams = RaycastParams.new() | ||
| CastParams.FilterDescendantsInstances = {} | ||
| CastParams.FilterType = Enum.RaycastFilterType.Exclude | ||
| CastParams.IgnoreWater = true | ||
|
|
||
| -- Behavior | ||
| local castBehavior = FastCast.newBehavior() | ||
| castBehavior.MaxDistance = 999999999 | ||
| castBehavior.RaycastParams = CastParams | ||
| castBehavior.HighFidelityBehavior = 1 | ||
| castBehavior.HighFidelitySegmentSize = 1 | ||
| castBehavior.Acceleration = Vector3.new(0, 0, 0) | ||
| castBehavior.AutoIgnoreContainer = true | ||
| castBehavior.CosmeticBulletContainer = ProjectileContainer | ||
| castBehavior.CosmeticBulletTemplate = ProjectileTemplate | ||
|
|
||
| -- Parallel Caster | ||
| local Caster = FastCast.newParallel() | ||
| Caster:Init( | ||
| 4, -- numWorkers | ||
| RepFirst, -- newParent | ||
| "CastVMs", -- newName | ||
| RepFirst, -- ContainerParent | ||
| "CastVMContainer", -- VMContainerName | ||
| "CastVM", -- VMname | ||
| true, -- useBulkMoveTo | ||
| nil, -- FastCastEventsModule (optional for parallel) | ||
| false, -- useObjectCache | ||
| nil, -- Template | ||
| 500, -- CacheSize | ||
| workspace -- CacheHolder | ||
| ) | ||
|
|
||
| local activeCasts = {} | ||
|
|
||
| Caster.CastFire:Connect(function(cast) | ||
| table.insert(activeCasts, cast) | ||
| end) | ||
|
|
||
| -- Functions | ||
| local function summary() | ||
| print(string.format("Delta: %.2f ms", 1000 / currentFps)) | ||
| print(string.format("Average FPS: %.2f", averageFps)) | ||
| print(string.format("Max FPS: %.2f", maxFps)) | ||
| print(string.format("Min FPS: %.2f", minFps)) | ||
| end | ||
|
|
||
| -- Benchmark | ||
| local isBenchmarking = false | ||
| local AMOUNT = 5000 | ||
| local BENCH_TIME = 5 | ||
|
|
||
| UIS.InputBegan:Connect(function(input, gp) | ||
| if gp then return end | ||
| if isBenchmarking then return end | ||
| if input.KeyCode == Enum.KeyCode.P then | ||
| isBenchmarking = true | ||
| print("=== PARALLEL MODE BENCHMARK ===") | ||
| print(string.format("Firing %d casts...", AMOUNT)) | ||
|
|
||
| for i = 1, AMOUNT do | ||
| Caster:RaycastFire( | ||
| Vector3.new( | ||
| math.random(-1, 1) * 5000, | ||
| math.random(-1, 1) * 5000, | ||
| math.random(-1, 1) * 5000 | ||
| ), | ||
| Vector3.new( | ||
| math.random(-1, 1) * 5000, | ||
| math.random(-1, 1) * 5000, | ||
| math.random(-1, 1) * 5000 | ||
| ), | ||
| 35, | ||
| castBehavior | ||
| ) | ||
| end | ||
|
|
||
| print("=== CREATION COMPLETE ===") | ||
| summary() | ||
|
|
||
| task.wait(BENCH_TIME) | ||
|
|
||
| print("=== SIMULATION COMPLETE ===") | ||
| summary() | ||
|
|
||
| print("=== CLEANUP ===") | ||
| for i = #activeCasts, 1, -1 do | ||
| Caster:TerminateCast(activeCasts[i]) | ||
| end | ||
| activeCasts = {} | ||
|
|
||
| print("=== DONE ===") | ||
| summary() | ||
| isBenchmarking = false | ||
| end | ||
| end) | ||
|
|
||
| print("Press P to start Parallel benchmark") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| -- Services | ||
| local RS = game:GetService("RunService") | ||
| local Rep = game:GetService("ReplicatedStorage") | ||
| local UIS = game:GetService("UserInputService") | ||
|
|
||
| -- Requires | ||
| local FastCast = require(Rep:WaitForChild("FastCast2")) | ||
|
|
||
| -- Variables | ||
| local ProjectileContainer = Instance.new("Folder") | ||
| ProjectileContainer.Name = "FastCast2PJ" | ||
| ProjectileContainer.Parent = workspace | ||
| local ProjectileTemplate = Instance.new("Part") | ||
| ProjectileTemplate.Name = "Projectile" | ||
| ProjectileTemplate.Parent = Rep | ||
| ProjectileTemplate.Size = Vector3.new(1,1,1) | ||
| ProjectileTemplate.CanCollide = false | ||
| ProjectileTemplate.Anchored = true | ||
| ProjectileTemplate.CanQuery = false | ||
| ProjectileTemplate.CanTouch = false | ||
| ProjectileTemplate.Position = Vector3.new(1,1,1) | ||
| ProjectileTemplate.Massless = true | ||
|
|
||
| -- FPS tracking | ||
| local startTime = tick() | ||
| local updateRate = 0.5 | ||
| local fpsTable = {} | ||
| local averageFps = 0 | ||
| local maxFps = 0 | ||
| local minFps = math.huge | ||
| local currentFps = 0 | ||
|
|
||
| RS.Heartbeat:Connect(function(dt: number) | ||
| local fps = 1/dt | ||
| currentFps = fps | ||
| if fps > maxFps then | ||
| maxFps = fps | ||
| end | ||
| if fps < minFps then | ||
| minFps = fps | ||
| end | ||
| table.insert(fpsTable, fps) | ||
|
|
||
| if tick() >= startTime + updateRate then | ||
| local totalFps = 0 | ||
| for _, vFps in fpsTable do | ||
| totalFps += vFps | ||
| end | ||
| averageFps = totalFps / #fpsTable | ||
| fpsTable = {} | ||
| startTime = tick() | ||
| end | ||
| end) | ||
|
|
||
| -- CastParams | ||
| local CastParams = RaycastParams.new() | ||
| CastParams.FilterDescendantsInstances = {} | ||
| CastParams.FilterType = Enum.RaycastFilterType.Exclude | ||
| CastParams.IgnoreWater = true | ||
|
|
||
| -- Behavior | ||
| local castBehavior = FastCast.newBehavior() | ||
| castBehavior.MaxDistance = 999999999 | ||
| castBehavior.RaycastParams = CastParams | ||
| castBehavior.HighFidelityBehavior = 1 | ||
| castBehavior.HighFidelitySegmentSize = 1 | ||
| castBehavior.Acceleration = Vector3.new(0, 0, 0) | ||
| castBehavior.AutoIgnoreContainer = true | ||
| castBehavior.CosmeticBulletContainer = ProjectileContainer | ||
| castBehavior.CosmeticBulletTemplate = ProjectileTemplate | ||
|
|
||
| -- Serial Caster | ||
| local Caster = FastCast.new() | ||
| Caster:Init( | ||
| true, -- useBulkMoveTo | ||
| false -- useObjectCache | ||
| ) | ||
|
|
||
| local activeCasts = {} | ||
|
|
||
| Caster.CastFire:Connect(function(cast) | ||
| table.insert(activeCasts, cast) | ||
| end) | ||
|
|
||
| -- Functions | ||
| local function summary() | ||
| print(string.format("Delta: %.2f ms", 1000 / currentFps)) | ||
| print(string.format("Average FPS: %.2f", averageFps)) | ||
| print(string.format("Max FPS: %.2f", maxFps)) | ||
| print(string.format("Min FPS: %.2f", minFps)) | ||
| end | ||
|
|
||
| -- Benchmark | ||
| local isBenchmarking = false | ||
| local AMOUNT = 5000 | ||
| local BENCH_TIME = 5 | ||
|
|
||
| UIS.InputBegan:Connect(function(input, gp) | ||
| if gp then return end | ||
| if isBenchmarking then return end | ||
| if input.KeyCode == Enum.KeyCode.E then | ||
| isBenchmarking = true | ||
| print("=== SERIAL MODE BENCHMARK ===") | ||
| print(string.format("Firing %d casts...", AMOUNT)) | ||
|
|
||
| for i = 1, AMOUNT do | ||
| local direction = Vector3.new( | ||
| math.random() * 2 - 1, | ||
| math.random() * 2 - 1, | ||
| math.random() * 2 - 1 | ||
| ) | ||
| if direction.Magnitude == 0 then | ||
| direction = Vector3.new(0, 0, 1) | ||
| end | ||
| Caster:RaycastFire( | ||
| Vector3.new( | ||
| math.random() * 2 - 1, | ||
| math.random() * 2 - 1, | ||
| math.random() * 2 - 1 | ||
| ) * 5000, | ||
| direction, | ||
| 35, | ||
| castBehavior | ||
| ) | ||
| end | ||
|
|
||
| print("=== CREATION COMPLETE ===") | ||
| summary() | ||
|
|
||
| task.wait(BENCH_TIME) | ||
|
|
||
| print("=== SIMULATION COMPLETE ===") | ||
| summary() | ||
|
|
||
| print("=== CLEANUP ===") | ||
| for i = #activeCasts, 1, -1 do | ||
| Caster:TerminateCast(activeCasts[i]) | ||
| end | ||
| activeCasts = {} | ||
|
|
||
| print("=== DONE ===") | ||
| summary() | ||
| isBenchmarking = false | ||
| end | ||
| end) | ||
|
|
||
| print("Press E to start benchmark") | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.