Skip to content

Introduce serial and parallel caster modes with SoA simulation architecture#39

Draft
weenachuangkud wants to merge 57 commits intomainfrom
major
Draft

Introduce serial and parallel caster modes with SoA simulation architecture#39
weenachuangkud wants to merge 57 commits intomainfrom
major

Conversation

@weenachuangkud
Copy link
Copy Markdown
Owner

@weenachuangkud weenachuangkud commented May 8, 2026

Summary by CodeRabbit

  • New Features

    • Added serial and parallel caster modes, Motor6D transform movement with pooling, and client-side serial/parallel FPS benchmarks.
  • Bug Fixes

    • Fixed high-fidelity sub-ray direction timestep calculation.
  • Documentation

    • Added full API reference, updated README (Rojo install + simplified examples), new architecture and comparison guides, and expanded changelog.
  • Chores

    • Updated TODOs and performed performance-focused refactor to SoA simulation and related optimizations.

- ActiveCastSerial: simplified, uses SerialSimulation
- BaseCastSerial: uses SerialSimulation for all casts
- SerialSimulation: single RunService, SoA, queue technique like SwiftCast
- Add ParallelSimulation: SoA pattern for each Actor
- Update BaseCast: uses ParallelSimulation instead of per-cast Heartbeat
- Each Actor now has one RunService handling multiple casts
- ActiveCast: Remove UpdateConnection (old per-cast heartbeat)
- ActiveCastSerial: Remove metatable, use pure data structure
- Now internal uses SoA, external exposes OOP API
- Add Motor6DPool for efficient Motor6D pooling
- Add MovementMethod to FastCastBehavior (BulkMoveTo/Transform)
- Update SerialSimulation to support Transform mode
- Per-caster configuration, not global
- Update ParallelSimulation with Motor6D Transform support
- Same as SerialSimulation implementation
- Added 0.1.0 changelog entry documenting new features
- Created Serial benchmark (benchSerial.client.luau)
- Created Parallel benchmark (benchParallel.client.luau)
- Updated TODO.md with all completed items
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b71933a-e683-4eb3-b02c-c866104875d2

📥 Commits

Reviewing files that changed from the base of the PR and between b6ae671 and 4afb6e8.

📒 Files selected for processing (21)
  • Benchmarks/benchParallel.client.luau
  • Benchmarks/benchSerial.client.luau
  • README.md
  • TODO.md
  • docs/api-reference.md
  • docs/changelog.md
  • skills/architecture.md
  • skills/comparison_activecast_parallelsimulation.md
  • skills/edge-cases-analysis.md
  • src/FastCast2/ActiveCast.luau
  • src/FastCast2/ActiveCastSerial.luau
  • src/FastCast2/BaseCast.luau
  • src/FastCast2/BaseCastSerial.luau
  • src/FastCast2/Configs.luau
  • src/FastCast2/DefaultConfigs.luau
  • src/FastCast2/FastCastEnums.luau
  • src/FastCast2/Motor6DPool.luau
  • src/FastCast2/ParallelSimulation.luau
  • src/FastCast2/SerialSimulation.luau
  • src/FastCast2/TypeDefinitions.luau
  • src/FastCast2/init.luau

📝 Walkthrough

Walkthrough

FastCast2 is refactored into distinct Serial and Parallel casters with SoA-driven SerialSimulation and ParallelSimulation. Trajectory storage consolidates to a single Trajectory. Motor6D pooling and ActiveCastSerial are added. Public API adds FastCast.new() (serial) and FastCast.newParallel() (parallel). Docs, README, benchmarks, and changelog updated.

Changes

Serial and Parallel Casting Architecture Refactor with SoA Simulation

Layer / File(s) Summary
Type Contracts & Trajectory Consolidation
src/FastCast2/TypeDefinitions.luau, src/FastCast2/DefaultConfigs.luau, src/FastCast2/Configs.luau, src/FastCast2/FastCastEnums.luau
CastStateInfo.TrajectoriesCastStateInfo.Trajectory; add default MovementMethod = "BulkMoveTo" and minor header/comment edits.
Main API & Constructors
src/FastCast2/init.luau
Split into FastCastSerial and FastCastParallel metatables; add FastCast.new() (serial) and FastCast.newParallel() (parallel); split behavior factories and per-mode method sets and lifecycles.
ActiveCast Core
src/FastCast2/ActiveCast.luau
ActiveCast now uses singular Trajectory; createCastData initializes StateInfo.Trajectory, sets RayInfo.FastCastEventsModule and RayInfo.MovementMethod, returns cast; fixes to DebrisAdd and high-fidelity substep timing.
ActiveCastSerial
src/FastCast2/ActiveCastSerial.luau
New serial cast object with kinematics helpers, RaycastParams cloning, debug visualization, and cast handlers.
BaseCastSerial Entrypoints
src/FastCast2/BaseCastSerial.luau
New serial base implementing Init, Raycast/Blockcast/Spherecast, builds castData, constructs ActiveCastSerial, registers with SerialSimulation, fires CastFire, and implements Bind/Terminate/Destroy and ObjectCache stub.
BaseCast Parallel Integration
src/FastCast2/BaseCast.luau
Parallel path updated to register created casts with ParallelSimulation.Register(cast), store actives by cast.ID, and fire CastFire using the local cast object.
ParallelSimulation (SoA)
src/FastCast2/ParallelSimulation.luau
New SoA-based ParallelSimulation: Register/Unregister, swap-compaction, per-step UpdateCasts deriving step ray directions, handling queries, updating cosmetics/Motor6D, queuing events, and Start/Stop wiring to RunService.
SerialSimulation (SoA)
src/FastCast2/SerialSimulation.luau
New SoA-driven SerialSimulation on Heartbeat: Register/Unregister, per-frame UpdateCasts advancing time, performing queries, updating cosmetic bullets (Motor6D supported), queuing events, and Start/Stop lifecycle.
Motor6D Pooling
src/FastCast2/Motor6DPool.luau
New Motor6D pooling module exposing Initialize, Get, Return, Connect, Disconnect and managing a single anchored pool for Transform movement.
Configs & Docs
src/FastCast2/Configs.luau, docs/*, README.md, TODO.md, skills/*
Add DebugLogging.AutomaticPerformance flag; add API reference, changelog 0.1.0, README Rojo/install and simplified examples, TODO updates, architecture/comparison/edge-case docs, and benchmark clients.
Benchmarks (Serial & Parallel)
Benchmarks/benchSerial.client.luau, Benchmarks/benchParallel.client.luau
Add benchmark clients sampling FPS and firing large numbers of casts (E for serial, P for parallel), tracking active casts and printing delta/average/max/min FPS summaries.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Bench as Benchmark Script
  participant Caster as FastCast Caster
  participant Sim as Serial/Parallel Simulation
  User->>Bench: press E or P
  Bench->>Caster: Fire AMOUNT casts
  Caster->>Sim: Register casts
  Sim->>Sim: process casts per frame
  Bench->>Caster: Terminate casts after BENCH_TIME
  Bench->>User: print FPS stats
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Last major update plans #38 — Implements the refactor items described (Trajectories → Trajectory, serial/parallel casters, Motor6D pooling, HighFidelity fix, and API/docs changes).

Poem

🐰
I hopped the repo, snipped and sewn,
Split casters, made trajectories one,
Motor6Ds queued in tidy store,
Serial hums while Parallel soars,
FastCast leaps — I chew some more.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: introducing dual caster modes (serial and parallel) with a structure-of-arrays simulation architecture, which aligns with the extensive refactoring across multiple core modules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch major

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/FastCast2/BaseCast.luau (1)

175-191: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Casts registered with ParallelSimulation but not stored in Actives table breaks cleanup and sync systems.

The cast created at lines 175-191 is registered with ParallelSimulation.Register(cast) but never added to the Actives table. However, Actives is still expected by:

  • HandleBulkMoveTo (line 56): Iterates Actives to batch-update cosmetic bullet positions
  • ActiveCastCleaner.Event (line 127): Expects Actives[activeCastID] to remove cleaned casts
  • SyncChanges.Event (line 139): Expects Actives[ID] to sync cast state
  • Destroy (line 368): Iterates Actives to terminate remaining casts

Additionally, ParallelSimulation.Terminate() does not fire the ActiveCastCleaner event when a cast completes. The cast object is passed both cleanup and sync event references during creation, but these events will never be triggered by the parallel simulation, leaving cleanup callbacks unreachable.

The same pattern exists in Blockcast (lines 238-255) and Spherecast (lines 282-299).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/BaseCast.luau` around lines 175 - 191, The new casts (created
in the Raycast/Blockcast/Spherecast paths using ActiveCast.createCastData and
then ParallelSimulation.Register(cast)) must be added into the Actives table and
have their completion trigger the existing cleanup/sync events; after you call
ParallelSimulation.Register(cast) add Actives[cast.ID] = cast, and ensure when
the parallel simulation signals a cast completion you fire
ActiveCastCleaner.Event(cast.ID) (and remove Actives[cast.ID]) and also fire
SyncChanges.Event(cast.ID) (or call the same cleanup callback path used
elsewhere) so ActiveCastCleaner, SyncChanges and Destroy/HandleBulkMoveTo logic
see and remove these casts; update the Raycast, Blockcast, and Spherecast blocks
accordingly and/or attach a termination callback from
ParallelSimulation.Register to invoke ActiveCastCleaner and SyncChanges for the
cast.
src/FastCast2/ActiveCast.luau (1)

367-380: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Finish the Trajectory rename inside SimulateCast().

These lines switch to local trajectory = cast.StateInfo.Trajectory, but the function still reads latestTrajectory later on. That variable is undefined here, so the first simulation step will nil-index on StartTime / Acceleration.

Suggested fix
 	local trajectory = cast.StateInfo.Trajectory
@@
-	totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime
+	totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime
@@
-	if typeof(latestTrajectory.Acceleration) ~= "Vector3" then
-		latestTrajectory.Acceleration = Vector3.new()
+	if typeof(trajectory.Acceleration) ~= "Vector3" then
+		trajectory.Acceleration = Vector3.new()
 	end

Also applies to: 416-418

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/ActiveCast.luau` around lines 367 - 380, In SimulateCast(), you
started using local trajectory = cast.StateInfo.Trajectory but left uses of
latestTrajectory later; replace every use of latestTrajectory with trajectory
(e.g., when computing totalDelta and accessing StartTime/Acceleration) so lines
that compute totalDelta, and any subsequent references (also around the block at
the other occurrence ~416-418) use trajectory.StartTime,
trajectory.Acceleration, etc.; verify GetPositionAtTime/GetVelocityAtTime calls
and TotalRuntime updates all reference trajectory instead of latestTrajectory.
README.md (1)

200-202: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use Hit, not RayHit, in the events module example.

The runtime lookup uses FastCastEvents.Hit; module.RayHit will never be invoked.

Suggested fix
-module.RayHit = function()
+module.Hit = function()
 	print("Hit!")
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 200 - 202, The example registers the wrong event
name; replace the handler assignment of module.RayHit with the runtime-expected
FastCastEvents.Hit by defining module.Hit (or assigning to
module[FastCastEvents.Hit]) so the runtime lookup calls it—update the example to
use module.Hit instead of module.RayHit and ensure the string/constant
FastCastEvents.Hit is referenced consistently with the rest of the module.
src/FastCast2/init.luau (2)

128-145: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Unify the StateInfo.Trajectory shape before using it here.

This file currently treats Trajectory as both an array (trajectories[index], table.insert(...)) and a single segment (trajectory.StartTime). It also writes through lastTrajectory, which is never defined in this scope. The first transform update on this path will hit a nil access or mutate the wrong structure.

Also applies to: 154-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/init.luau` around lines 128 - 145, GetTrajectoryInfo and
GetLatestTrajectoryEndInfo assume StateInfo.Trajectory is an array but elsewhere
it may be a single segment and code writes through lastTrajectory (undefined
here), causing nil/mutation bugs; fix by normalizing StateInfo.Trajectory to a
consistent array shape before use (e.g., if StateInfo.Trajectory is a map with
StartTime, wrap it in a one-element array) and update any writers to always
push/replace elements on that array, and remove or properly define
lastTrajectory (use the local variable trajectory or StateInfo.Trajectory[#]
instead) so GetTrajectoryInfo(GetLatestTrajectoryEndInfo) never indexes a
non-array or mutates the wrong structure.

645-651: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return after warning when the parallel caster is not initialized.

This guard only logs, then immediately calls self.Dispatcher:DispatchAll(...) anyway. If self.Dispatcher is nil, SetBulkMoveEnabled() still crashes on the next line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/init.luau` around lines 645 - 651, In SetBulkMoveEnabled, the
guard only warns when the caster isn't initialized but proceeds to call
self.Dispatcher:DispatchAll and set self.BulkMoveEnabled, causing a crash if
self.Dispatcher is nil; update the function
(FastCastParallel:SetBulkMoveEnabled) to return immediately after the warn (or
otherwise check for self.Dispatcher) so DispatchAll and setting BulkMoveEnabled
only run when self.AlreadyInit and self.Dispatcher are valid.
🧹 Nitpick comments (2)
src/FastCast2/Motor6DPool.luau (1)

46-53: 💤 Low value

Consider simplifying Get() to avoid recursion.

The recursive call after GrowPool works correctly but could be simplified with a direct return.

Simplified version
 local function Get(): Motor6D
-	if `#FreeMotor6Ds` > 0 then
-		return table.remove(FreeMotor6Ds) :: Motor6D
-	else
+	if `#FreeMotor6Ds` == 0 then
 		GrowPool(PoolSize * GROWTH_RATE)
-		return Get()
 	end
+	return table.remove(FreeMotor6Ds) :: Motor6D
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/Motor6DPool.luau` around lines 46 - 53, The Get function
currently recurses after calling GrowPool; replace that recursion with direct
logic: call GrowPool(PoolSize * GROWTH_RATE) when FreeMotor6Ds is empty and then
immediately remove and return a Motor6D from FreeMotor6Ds. Update the Get
function to check FreeMotor6Ds, call GrowPool if empty, then perform a single
table.remove(FreeMotor6Ds) :: Motor6D return; keep references to FreeMotor6Ds,
GrowPool, PoolSize and GROWTH_RATE intact.
Benchmarks/benchSerial.client.luau (1)

55-70: 💤 Low value

Consider adding ProjectileContainer to filter exclusions.

The FilterDescendantsInstances is empty, but castBehavior.AutoIgnoreContainer = true should handle this. However, explicitly adding the container ensures it's excluded even before AutoIgnore processes it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Benchmarks/benchSerial.client.luau` around lines 55 - 70, The RaycastParams
FilterDescendantsInstances is left empty which can let the ProjectileContainer
be hit before AutoIgnoreContainer runs; update
CastParams.FilterDescendantsInstances to include the ProjectileContainer
instance so the container is explicitly excluded from raycasts (adjust the
CastParams used by castBehavior), i.e. add ProjectileContainer to
CastParams.FilterDescendantsInstances so CastParams (used by
castBehavior.RaycastParams) will proactively ignore that container.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Benchmarks/benchSerial.client.luau`:
- Around line 106-121: The current random direction uses integer
math.random(-1,1) which can produce a zero vector; update the Caster:RaycastFire
call to build float random components instead (e.g. x = (math.random() * 2 - 1)
* 5000) and normalize the Vector3 before passing it, and if the normalized
vector has zero magnitude replace it with a safe fallback direction (e.g.
Vector3.new(0,0,1)); apply these changes where Caster:RaycastFire is invoked so
the direction passed to RaycastFire is never a zero vector and maintains
expected length handling with the 35 parameter and castBehavior.

In `@README.md`:
- Around line 96-100: The README example uses FastCastEnums but never defines or
requires it, so copy-pasting fails; update the example to first require or
define FastCastEnums (the same module providing enums used by FastCast2) before
creating the behavior—e.g., add a line showing how to obtain FastCastEnums (the
module that exposes HighFidelityBehavior) and then keep the existing code that
calls FastCast2.newBehavior() and sets behavior.HighFidelityBehavior =
FastCastEnums.HighFidelityBehavior.Default so the snippet runs as-is.

In `@src/FastCast2/ActiveCast.luau`:
- Around line 854-857: High-fidelity branch still reads from latestTrajectory
instead of the new local trajectory, causing the crash in
HighFidelityBehavior.Always; update the Always branch to use the local variable
trajectory (from cast.StateInfo.Trajectory, where you already default
trajectory.Acceleration) when pulling origin/startTime/velocity/acceleration
(replace references to latestTrajectory in the HighFidelityBehavior.Always code
path with trajectory) so the branch uses the validated trajectory object.

In `@src/FastCast2/ActiveCastSerial.luau`:
- Around line 148-156: The RayInfo table in ActiveCastSerial.luau doesn't copy
shape fields, so SerialSimulation.Register (which reads cast.RayInfo.Size and
cast.RayInfo.Radius for Blockcast/Spherecast) sees nil; update the constructor
that builds RayInfo to include Size = castData.Size and Radius = castData.Radius
(or the appropriate source names on castData) so Blockcast and Spherecast have
their shape parameters when Type is set via CastVariantTypes[castData.CastType];
ensure the names match what SerialSimulation.Register expects.

In `@src/FastCast2/BaseCastSerial.luau`:
- Around line 32-45: The module-level mutable state (Output, ParentCaster,
ObjectCache, BulkMoveToConnection, NextProjectileID) must be made instance-local
so multiple BaseCastSerial instances do not share or clobber each other; modify
BaseCastSerial.Init to set self.Output, self.ParentCaster, self.ObjectCache,
self.BulkMoveToConnection and self.NextProjectileID (and initialize them
appropriately) instead of writing the module-scope variables, then update all
functions in BaseCastSerial that currently reference Output, ParentCaster,
ObjectCache, BulkMoveToConnection or NextProjectileID to use self.Output,
self.ParentCaster, self.ObjectCache, self.BulkMoveToConnection and
self.NextProjectileID so every caster keeps its own event sink/cache/parent and
ID counter.

In `@src/FastCast2/init.luau`:
- Around line 919-935: FastCastSerial:TerminateCast currently only disconnects
local state and destroys cosmetic objects but never delegates termination to the
base serial, leaving the cast registered; update FastCastSerial:TerminateCast to
call self.BaseCast:TerminateCast(cast, castTerminatingFunction) (instead of
directly invoking castTerminatingFunction) after performing the local cleanup
(disconnecting UpdateConnection and destroying CosmeticBulletObject) and before
firing self.Output:Fire("CastTerminating", cast) so the cast is properly
deregistered from SerialSimulation/BaseCast.
- Around line 815-818: GetVelocityCast in FastCastSerial currently returns the
trajectory's InitialVelocity, which ignores acceleration and elapsed time;
update GetVelocityCast(cast: vaildcast) to compute velocity at runtime by using
cast.StateInfo.Trajectory and the elapsed time (use TotalRuntime - StartTime
from the same cast.StateInfo like the position helper does), then evaluate the
trajectory's velocity at that elapsed time and return it instead of
InitialVelocity so accelerating serial casts report the current velocity.
- Around line 847-903: The setters/adders (SetVelocityCast, SetAccelerationCast,
SetPositionCast, AddPositionCast, AddVelocityCast, AddAccelerationCast) mutate
cast.StateInfo.Trajectory in-place without advancing StartTime to the cast's
TotalRuntime, causing retroactive path changes; before changing
Origin/InitialVelocity/Acceleration you must "rebase" the serial trajectory by
integrating the current Trajectory forward to cast.StateInfo.TotalRuntime
(compute the new Origin at that time), set Trajectory.StartTime =
cast.StateInfo.TotalRuntime, and replace
Trajectory.Origin/InitialVelocity/Acceleration with values relative to that new
StartTime (or add deltas for the Add* methods) so subsequent solves treat the
change as occurring only from the current runtime onward. Ensure this rebase
logic is invoked from each of the listed methods (or factor into a helper like
RebaseSerialTrajectory(cast) and call it first).
- Around line 1034-1044: The constructors FastCast.new and FastCast.newParallel
build caster tables (e.g., local fs = { ... } and local fs = { ... }) and set
their metatables (setmetatable(fs, FastCastSerial) / setmetatable(fs,
FastCastParallel)), but currently don't return them; update both functions to
return the constructed fs at the end so callers receive the caster instance
(ensure return fs is added after setmetatable in both FastCast.new and
FastCast.newParallel).
- Around line 721-733: The serial BindableEvent created as BindableOutput is not
wired to the public signals after calling BaseCastSerial.Init, so events like
"CastFire" and "CastTerminating" emitted by self.BaseCast never reach consumers;
add a listener on BindableOutput.Event (after self.BaseCast =
BaseCastSerial.Init(...)) that inspects the incoming event name and forwards it
to the corresponding public signal on this instance (for example, map "CastFire"
-> self.CastFire:Fire(...), "CastTerminating" -> self.CastTerminating:Fire(...),
and forward any other serial event names similarly) so that
FastCastSerial:Init/instance exposes the same callbacks as BaseCastSerial.

In `@src/FastCast2/ParallelSimulation.luau`:
- Around line 263-270: The call to CastHandler is passing a table named variant
for all cast types but Blockcast expects a Vector3 and Spherecast expects a
number; change how variant is constructed so that for EnumCastTypes.Blockcast
you set variant to castSize[i] (a Vector3) and for EnumCastTypes.Spherecast set
variant to castRadius[i] (a number), leaving the existing table form only for
other cast types if needed, then call CastHandler(castWorldRoot[i],
lastPosition, rayDir, castRaycastParams[i], variant).
- Around line 313-316: The loop that calls ParallelSimulation.Terminate over
destroyedIds must iterate in descending numeric order because Unregister() uses
swap-delete and removing lower indices first can change positions of higher
indices; update the code that processes destroyedIds (the block calling
ParallelSimulation.Terminate) to sort or iterate destroyedIds from highest to
lowest (e.g., iterate numeric indices in reverse) so Terminate is invoked on IDs
in descending order to avoid invalidating queued ids after Unregister().
- Around line 161-221: The removal logic swaps the tail (lastId) into slot id
but then disconnects and clears fields from id (corrupting the moved cast); fix
by treating lastId as the source: when id ~= lastId assign casts[id] =
casts[lastId] (in addition to the per-field arrays) and update casts[lastId].ID
= id, then call Motor6DPool.Disconnect(castMotor6D[lastId]) and nil out all
cast*[...] entries using lastId (not id) before decrementing castCount; ensure
castCFrame and castMotor6D clears reference lastId so the moved cast in slot id
remains intact.
- Around line 321-324: ParallelSimulation.Start currently connects RS.PreRender
(client-only) which will error on servers; modify ParallelSimulation.Start to
first check the RunService client/server state (use RS:IsClient() or similar)
and only call RS.PreRender:ConnectParallel(UpdateCasts) when running on the
client, or alternatively use RS.Heartbeat for server-side behavior; ensure you
still set and guard ParallelSimulation.StepConnection and early-return if
already connected. This change keeps ParallelSimulation.Start, UpdateCasts, and
ParallelSimulation.StepConnection as the anchors for the fix.

In `@src/FastCast2/SerialSimulation.luau`:
- Around line 317-320: The loop that calls SerialSimulation.Terminate for each
id in destroyedIds must iterate destroyedIds in descending index order to avoid
invalidation caused by swap-delete compaction; change the iteration over
destroyedIds so you process highest indices first (e.g., iterate from
`#destroyedIds` down to 1) and call SerialSimulation.Terminate(id) in that
descending order to ensure removals do not shift and invalidate later ids.
- Around line 263-274: The code always passes the variant table to CastHandler,
but Blockcast and Spherecast handlers expect a raw Vector3/number; update the
call so it passes variant.Size for EnumCastTypes.Blockcast and variant.Radius
for EnumCastTypes.Spherecast (use castSize[i] and castRadius[i] or the variant
fields you already set) instead of the whole variant table when castType is
Blockcast or Spherecast; keep passing variant only for other cast types. Ensure
this change is applied at the CastHandler(...) invocation so the correct shape
argument is forwarded to block/sphere handlers.
- Around line 164-225: The code in Unregister swaps the tail row into id but
then disconnects and clears entries at index id (which now holds the moved
cast), causing the moved cast to be corrupted; change the cleanup to operate on
lastId instead: perform Motor6DPool.Disconnect(castMotor6D[lastId]) and set
castIDs[lastId], castOrigin[lastId], ..., castCFrame[lastId],
castMotor6D[lastId], casts[lastId] = nil, then set castCount = lastId - 1. Keep
the earlier copy/ID update that moves lastId into id, but ensure all nil
assignments and the disconnect target lastId (not id) so the moved cast at id
remains intact.

---

Outside diff comments:
In `@README.md`:
- Around line 200-202: The example registers the wrong event name; replace the
handler assignment of module.RayHit with the runtime-expected FastCastEvents.Hit
by defining module.Hit (or assigning to module[FastCastEvents.Hit]) so the
runtime lookup calls it—update the example to use module.Hit instead of
module.RayHit and ensure the string/constant FastCastEvents.Hit is referenced
consistently with the rest of the module.

In `@src/FastCast2/ActiveCast.luau`:
- Around line 367-380: In SimulateCast(), you started using local trajectory =
cast.StateInfo.Trajectory but left uses of latestTrajectory later; replace every
use of latestTrajectory with trajectory (e.g., when computing totalDelta and
accessing StartTime/Acceleration) so lines that compute totalDelta, and any
subsequent references (also around the block at the other occurrence ~416-418)
use trajectory.StartTime, trajectory.Acceleration, etc.; verify
GetPositionAtTime/GetVelocityAtTime calls and TotalRuntime updates all reference
trajectory instead of latestTrajectory.

In `@src/FastCast2/BaseCast.luau`:
- Around line 175-191: The new casts (created in the
Raycast/Blockcast/Spherecast paths using ActiveCast.createCastData and then
ParallelSimulation.Register(cast)) must be added into the Actives table and have
their completion trigger the existing cleanup/sync events; after you call
ParallelSimulation.Register(cast) add Actives[cast.ID] = cast, and ensure when
the parallel simulation signals a cast completion you fire
ActiveCastCleaner.Event(cast.ID) (and remove Actives[cast.ID]) and also fire
SyncChanges.Event(cast.ID) (or call the same cleanup callback path used
elsewhere) so ActiveCastCleaner, SyncChanges and Destroy/HandleBulkMoveTo logic
see and remove these casts; update the Raycast, Blockcast, and Spherecast blocks
accordingly and/or attach a termination callback from
ParallelSimulation.Register to invoke ActiveCastCleaner and SyncChanges for the
cast.

In `@src/FastCast2/init.luau`:
- Around line 128-145: GetTrajectoryInfo and GetLatestTrajectoryEndInfo assume
StateInfo.Trajectory is an array but elsewhere it may be a single segment and
code writes through lastTrajectory (undefined here), causing nil/mutation bugs;
fix by normalizing StateInfo.Trajectory to a consistent array shape before use
(e.g., if StateInfo.Trajectory is a map with StartTime, wrap it in a one-element
array) and update any writers to always push/replace elements on that array, and
remove or properly define lastTrajectory (use the local variable trajectory or
StateInfo.Trajectory[#] instead) so
GetTrajectoryInfo(GetLatestTrajectoryEndInfo) never indexes a non-array or
mutates the wrong structure.
- Around line 645-651: In SetBulkMoveEnabled, the guard only warns when the
caster isn't initialized but proceeds to call self.Dispatcher:DispatchAll and
set self.BulkMoveEnabled, causing a crash if self.Dispatcher is nil; update the
function (FastCastParallel:SetBulkMoveEnabled) to return immediately after the
warn (or otherwise check for self.Dispatcher) so DispatchAll and setting
BulkMoveEnabled only run when self.AlreadyInit and self.Dispatcher are valid.

---

Nitpick comments:
In `@Benchmarks/benchSerial.client.luau`:
- Around line 55-70: The RaycastParams FilterDescendantsInstances is left empty
which can let the ProjectileContainer be hit before AutoIgnoreContainer runs;
update CastParams.FilterDescendantsInstances to include the ProjectileContainer
instance so the container is explicitly excluded from raycasts (adjust the
CastParams used by castBehavior), i.e. add ProjectileContainer to
CastParams.FilterDescendantsInstances so CastParams (used by
castBehavior.RaycastParams) will proactively ignore that container.

In `@src/FastCast2/Motor6DPool.luau`:
- Around line 46-53: The Get function currently recurses after calling GrowPool;
replace that recursion with direct logic: call GrowPool(PoolSize * GROWTH_RATE)
when FreeMotor6Ds is empty and then immediately remove and return a Motor6D from
FreeMotor6Ds. Update the Get function to check FreeMotor6Ds, call GrowPool if
empty, then perform a single table.remove(FreeMotor6Ds) :: Motor6D return; keep
references to FreeMotor6Ds, GrowPool, PoolSize and GROWTH_RATE intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a57e535-492e-4838-826f-86da925a99ab

📥 Commits

Reviewing files that changed from the base of the PR and between b6ae671 and 82519af.

📒 Files selected for processing (18)
  • Benchmarks/benchParallel.client.luau
  • Benchmarks/benchSerial.client.luau
  • README.md
  • TODO.md
  • docs/api-reference.md
  • docs/changelog.md
  • src/FastCast2/ActiveCast.luau
  • src/FastCast2/ActiveCastSerial.luau
  • src/FastCast2/BaseCast.luau
  • src/FastCast2/BaseCastSerial.luau
  • src/FastCast2/Configs.luau
  • src/FastCast2/DefaultConfigs.luau
  • src/FastCast2/FastCastEnums.luau
  • src/FastCast2/Motor6DPool.luau
  • src/FastCast2/ParallelSimulation.luau
  • src/FastCast2/SerialSimulation.luau
  • src/FastCast2/TypeDefinitions.luau
  • src/FastCast2/init.luau

Comment thread Benchmarks/benchSerial.client.luau
Comment thread README.md
Comment thread src/FastCast2/ActiveCast.luau
Comment thread src/FastCast2/ActiveCastSerial.luau
Comment thread src/FastCast2/BaseCastSerial.luau Outdated
Comment thread src/FastCast2/ParallelSimulation.luau Outdated
Comment thread src/FastCast2/ParallelSimulation.luau
Comment thread src/FastCast2/SerialSimulation.luau
Comment thread src/FastCast2/SerialSimulation.luau
Comment thread src/FastCast2/SerialSimulation.luau Outdated
@weenachuangkud weenachuangkud self-assigned this May 8, 2026
@weenachuangkud weenachuangkud added documentation Improvements or additions to documentation enhancement New feature or request labels May 8, 2026
…ajectory setters, update ModifyTransformation
@weenachuangkud
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/FastCast2/init.luau (1)

998-1011: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard FastCastParallel:Destroy() when Init() never completed.

Line 1010 assumes self.Dispatcher exists, but new parallel casters start with Dispatcher = nil. Calling Destroy() before a successful Init() will error during cleanup.

🛡️ Minimal fix
-	self.Dispatcher:Destroy()
+	if self.Dispatcher then
+		self.Dispatcher:Destroy()
+		self.Dispatcher = nil
+	end
 	setmetatable(self, nil)
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/init.luau` around lines 998 - 1011, FastCastParallel:Destroy()
assumes Init() completed and unconditionally calls self.Dispatcher:Destroy();
change Destroy to guard against partially-initialized instances by nil-checking
any optional fields before calling methods on them (at minimum check
self.Dispatcher and self.ObjectCache exist before calling :Destroy()), and only
call DestroySignal on signals that are non-nil; keep setmetatable(self, nil) at
the end so cleanup still completes for partially-initialized objects.
♻️ Duplicate comments (2)
src/FastCast2/ParallelSimulation.luau (2)

264-271: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pass raw shape values to the block/sphere cast handlers.

Lines 264-271 always pass a table as the fifth argument, but Blockcast expects a Vector3 and Spherecast expects a number. Parallel block/sphere casts will fail until the raw size/radius is forwarded.

🔧 Minimal fix
-		local variant = {}
-		if castType == EnumCastTypes.Blockcast then
-			variant.Size = castSize[i]
-		elseif castType == EnumCastTypes.Spherecast then
-			variant.Radius = castRadius[i]
-		end
-
-		local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], variant)
+		local result
+		if castType == EnumCastTypes.Blockcast then
+			result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], castSize[i] :: Vector3)
+		elseif castType == EnumCastTypes.Spherecast then
+			result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], castRadius[i] :: number)
+		else
+			result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i])
+		end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/ParallelSimulation.luau` around lines 264 - 271, The code
always builds a table `variant` and passes it to `CastHandler`, but
`EnumCastTypes.Blockcast` expects a raw Vector3 (`castSize[i]`) and
`EnumCastTypes.Spherecast` expects a raw number (`castRadius[i]`); change the
call site so when `castType` equals `EnumCastTypes.Blockcast` pass `castSize[i]`
as the fifth argument and when `EnumCastTypes.Spherecast` pass `castRadius[i]`,
otherwise build/pass the `variant` table as before; update the call to
`CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i],
<appropriate value>)` accordingly so block/sphere casts receive the raw shape
values.

161-221: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Preserve the swapped-in cast during Unregister().

This swap-delete path still tears down the destination slot after moving lastId into id: the moved cast loses its CFrame, casts[id] is nilled, and its Motor6D is disconnected, while tail removals leak their own motor. Clear/disconnect the removed row, not the destination row.

🧩 Minimal fix
 function ParallelSimulation.Unregister(id: number)
 	if not casts[id] then return end

 	local lastId = castCount
+	local removedMotor6D = castMotor6D[id]
+	local movedCast = casts[lastId]
 	if id ~= lastId then
+		casts[id] = movedCast
 		castIDs[id] = castIDs[lastId]
 		castOrigin[id] = castOrigin[lastId]
 		castVelocity[id] = castVelocity[lastId]
@@
 		castCaster[id] = castCaster[lastId]
 		castMotor6D[id] = castMotor6D[lastId]

-		if casts[lastId] then
-			casts[lastId].ID = id
-			casts[id] = casts[lastId]
+		if movedCast then
+			movedCast.ID = id
 		end
 	end

-	if id ~= lastId and castMotor6D[lastId] then
-		Motor6DPool.Disconnect(castMotor6D[lastId])
+	if removedMotor6D then
+		Motor6DPool.Disconnect(removedMotor6D)
 	end
@@
-	castCFrame[id] = nil
+	castCFrame[lastId] = nil
@@
-	casts[id] = nil
+	casts[lastId] = nil
 	castCount = lastId - 1
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/ParallelSimulation.luau` around lines 161 - 221, In
Unregister(), the cleanup is tearing down the destination slot after a swap:
change the cleanup to clear the removed row (lastId) instead of id — fix the
mistaken castCFrame[id] = nil to castCFrame[lastId] = nil and set casts[lastId]
= nil (not casts[id]); also only call Motor6DPool.Disconnect for the
tail-removal case (when id == lastId) so we don't disconnect the Motor6D that
was moved into id (adjust the conditional around
Motor6DPool.Disconnect(castMotor6D[lastId]) accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/FastCast2/BaseCastSerial.luau`:
- Around line 106-110: The serial Raycast path is using incorrect
double-dereferences (self.self) which will error before registration or
emitting; update the references in the Serial path to use the same instance
fields used by Blockcast/Spherecast (replace self.self.ParentCaster with
self.ParentCaster when calling ActiveCastSerial.new, and replace
self.self.Output with self.Output before calling :Fire("CastFire", ...)),
keeping the existing calls to SerialSimulation.Register(cast) and the CastFire
emission intact.

In `@src/FastCast2/init.luau`:
- Around line 921-929: The TerminateCast path currently calls
self.BaseCast:TerminateCast(cast, castTerminatingFunction) and then
unconditionally calls castTerminatingFunction(cast) and
self.Output:Fire("CastTerminating", cast), causing duplicate serial callbacks
and signals; change the logic so that if self.BaseCast exists you only delegate
to BaseCast:TerminateCast(...) and do not call castTerminatingFunction or fire
the output again, and if self.BaseCast is nil then call
castTerminatingFunction(cast) (if present) and fire
self.Output:Fire("CastTerminating", cast) exactly once. Ensure the unique
symbols are BaseCast:TerminateCast, castTerminatingFunction, and
self.Output:Fire("CastTerminating", cast).

---

Outside diff comments:
In `@src/FastCast2/init.luau`:
- Around line 998-1011: FastCastParallel:Destroy() assumes Init() completed and
unconditionally calls self.Dispatcher:Destroy(); change Destroy to guard against
partially-initialized instances by nil-checking any optional fields before
calling methods on them (at minimum check self.Dispatcher and self.ObjectCache
exist before calling :Destroy()), and only call DestroySignal on signals that
are non-nil; keep setmetatable(self, nil) at the end so cleanup still completes
for partially-initialized objects.

---

Duplicate comments:
In `@src/FastCast2/ParallelSimulation.luau`:
- Around line 264-271: The code always builds a table `variant` and passes it to
`CastHandler`, but `EnumCastTypes.Blockcast` expects a raw Vector3
(`castSize[i]`) and `EnumCastTypes.Spherecast` expects a raw number
(`castRadius[i]`); change the call site so when `castType` equals
`EnumCastTypes.Blockcast` pass `castSize[i]` as the fifth argument and when
`EnumCastTypes.Spherecast` pass `castRadius[i]`, otherwise build/pass the
`variant` table as before; update the call to `CastHandler(castWorldRoot[i],
lastPosition, rayDir, castRaycastParams[i], <appropriate value>)` accordingly so
block/sphere casts receive the raw shape values.
- Around line 161-221: In Unregister(), the cleanup is tearing down the
destination slot after a swap: change the cleanup to clear the removed row
(lastId) instead of id — fix the mistaken castCFrame[id] = nil to
castCFrame[lastId] = nil and set casts[lastId] = nil (not casts[id]); also only
call Motor6DPool.Disconnect for the tail-removal case (when id == lastId) so we
don't disconnect the Motor6D that was moved into id (adjust the conditional
around Motor6DPool.Disconnect(castMotor6D[lastId]) accordingly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 700c67d6-f5c7-4a96-b320-98347bb4b2dd

📥 Commits

Reviewing files that changed from the base of the PR and between 51225b1 and 58c9792.

📒 Files selected for processing (6)
  • README.md
  • src/FastCast2/ActiveCast.luau
  • src/FastCast2/ActiveCastSerial.luau
  • src/FastCast2/BaseCastSerial.luau
  • src/FastCast2/ParallelSimulation.luau
  • src/FastCast2/init.luau
✅ Files skipped from review due to trivial changes (1)
  • src/FastCast2/ActiveCastSerial.luau
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • src/FastCast2/ActiveCast.luau

Comment thread src/FastCast2/BaseCastSerial.luau
Comment thread src/FastCast2/init.luau
Comment on lines +921 to +929
if self.BaseCast then
self.BaseCast:TerminateCast(cast, castTerminatingFunction)
end

if castTerminatingFunction then
castTerminatingFunction(cast)
end

self.Output:Fire("CastTerminating", cast)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid double-firing serial termination callbacks and signals.

self.BaseCast:TerminateCast(...) already invokes castTerminatingFunction and emits "CastTerminating" through self.Output. Lines 925-929 repeat both, so serial cleanup logic and listeners run twice for the same cast.

🧹 Minimal fix
 	if self.BaseCast then
 		self.BaseCast:TerminateCast(cast, castTerminatingFunction)
-	end
-
-	if castTerminatingFunction then
-		castTerminatingFunction(cast)
-	end
-
-	self.Output:Fire("CastTerminating", cast)
+	elseif castTerminatingFunction then
+		castTerminatingFunction(cast)
+		if self.Output then
+			self.Output:Fire("CastTerminating", cast)
+		end
+	end
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if self.BaseCast then
self.BaseCast:TerminateCast(cast, castTerminatingFunction)
end
if castTerminatingFunction then
castTerminatingFunction(cast)
end
self.Output:Fire("CastTerminating", cast)
if self.BaseCast then
self.BaseCast:TerminateCast(cast, castTerminatingFunction)
elseif castTerminatingFunction then
castTerminatingFunction(cast)
if self.Output then
self.Output:Fire("CastTerminating", cast)
end
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/FastCast2/init.luau` around lines 921 - 929, The TerminateCast path
currently calls self.BaseCast:TerminateCast(cast, castTerminatingFunction) and
then unconditionally calls castTerminatingFunction(cast) and
self.Output:Fire("CastTerminating", cast), causing duplicate serial callbacks
and signals; change the logic so that if self.BaseCast exists you only delegate
to BaseCast:TerminateCast(...) and do not call castTerminatingFunction or fire
the output again, and if self.BaseCast is nil then call
castTerminatingFunction(cast) (if present) and fire
self.Output:Fire("CastTerminating", cast) exactly once. Ensure the unique
symbols are BaseCast:TerminateCast, castTerminatingFunction, and
self.Output:Fire("CastTerminating", cast).

@weenachuangkud
Copy link
Copy Markdown
Owner Author

@coderabbitai Write me a single prompt for ai agent

  • FastCastSerial having feature conflict with FastCastParallel
  • ActiveCastSerial: 123, why castData? No explicit parameters like in ActiveCast.luau .createCastData(...)
  • BaseCastSerial: 36 -> 46, unused parameters: Data
  • SerialSimulation.luau: 21 -> 32, If you do not use these constants, consider removing it.
  • ParallelSimulation: 19 -> 30, Are these unused constants? If so, remove it
  • Lints more if there any issues

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me read the relevant files first before generating the prompt.


🏁 Script executed:

#!/bin/bash
echo "=== src/FastCast2/ActiveCastSerial.luau (around line 123) ==="
sed -n '110,140p' src/FastCast2/ActiveCastSerial.luau

echo ""
echo "=== src/FastCast2/BaseCastSerial.luau (lines 30-60) ==="
sed -n '30,60p' src/FastCast2/BaseCastSerial.luau

echo ""
echo "=== src/FastCast2/SerialSimulation.luau (lines 15-45) ==="
sed -n '15,45p' src/FastCast2/SerialSimulation.luau

echo ""
echo "=== src/FastCast2/ParallelSimulation.luau (lines 15-40) ==="
sed -n '15,40p' src/FastCast2/ParallelSimulation.luau

echo ""
echo "=== src/FastCast2/init.luau (FastCastSerial vs FastCastParallel method tables) ==="
grep -n "FastCastSerial\.\|FastCastParallel\." src/FastCast2/init.luau | head -80

echo ""
echo "=== src/FastCast2/ActiveCast.luau createCastData ==="
grep -n "createCastData\|function.*castData" src/FastCast2/ActiveCast.luau | head -20

echo ""
echo "=== Full ActiveCastSerial.luau ==="
cat src/FastCast2/ActiveCastSerial.luau

echo ""
echo "=== Full SerialSimulation.luau lines 1-50 ==="
sed -n '1,55p' src/FastCast2/SerialSimulation.luau

echo ""
echo "=== Full ParallelSimulation.luau lines 1-50 ==="
sed -n '1,50p' src/FastCast2/ParallelSimulation.luau

echo ""
echo "=== Full BaseCastSerial.luau ==="
cat src/FastCast2/BaseCastSerial.luau

echo ""
echo "=== Full init.luau ==="
cat src/FastCast2/init.luau

Length of output: 54706


I need to check the full body of both simulation files to confirm which constants are actually used.


🏁 Script executed:

#!/bin/bash
echo "=== SerialSimulation.luau full ==="
cat src/FastCast2/SerialSimulation.luau

echo ""
echo "=== ParallelSimulation.luau full ==="
cat src/FastCast2/ParallelSimulation.luau

Length of output: 22229


@weenachuangkud Here is the comprehensive AI agent prompt:


FastCast2 AI Agent Prompt — Cleanup & Bug Fixes

Fix all of the following issues across src/FastCast2. Apply every change exactly as described.


Fix 1 — src/FastCast2/init.luau: FastCastSerial uses wrong newBehavior()

FastCastSerial:RaycastFire, BlockcastFire, and SpherecastFire all fall back to FastCastParallel.newBehavior() when BehaviorData is nil. Change all three to use FastCastSerial.newBehavior():

 function FastCastSerial:RaycastFire(...)
   if BehaviorData == nil then
-    BehaviorData = FastCastParallel.newBehavior()
+    BehaviorData = FastCastSerial.newBehavior()
   end
   ...
 end

 function FastCastSerial:BlockcastFire(...)
   if BehaviorData == nil then
-    BehaviorData = FastCastParallel.newBehavior()
+    BehaviorData = FastCastSerial.newBehavior()
   end
   ...
 end

 function FastCastSerial:SpherecastFire(...)
   if BehaviorData == nil then
-    BehaviorData = FastCastParallel.newBehavior()
+    BehaviorData = FastCastSerial.newBehavior()
   end
   ...
 end

Fix 2 — src/FastCast2/init.luau: Remove stale UpdateConnection guards from FastCastParallel methods

The SoA parallel simulation never sets UpdateConnection on any cast. Every assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") in FastCastParallel will always error. Remove that assert from every method that has it: GetVelocityCast, GetAccelerationCast, GetPositionCast, SetVelocityCast, SetAccelerationCast, SetPositionCast, PauseCast, AddPositionCast, AddVelocityCast, AddAccelerationCast, SyncChangesToCast:

 function FastCastParallel:GetVelocityCast(cast: vaildcast)
-  assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED")
   local currentTrajectory = cast.StateInfo.Trajectory
   ...
 end
 -- (repeat for every other method listed above)

Fix 3 — src/FastCast2/init.luau: FastCastSerial:TerminateCast double-fires and double-calls

FastCastSerial:TerminateCast calls self.BaseCast:TerminateCast(cast, castTerminatingFunction), which already fires Output:Fire("CastTerminating", cast) and calls castTerminatingFunction(cast). Then it calls both again directly, causing double emission and a double callback. Remove the redundant direct calls:

 function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?)
   if cast.StateInfo.UpdateConnection ~= nil then
     cast.StateInfo.UpdateConnection:Disconnect()
     cast.StateInfo.UpdateConnection = nil
   end

   if cast.RayInfo.CosmeticBulletObject then
     cast.RayInfo.CosmeticBulletObject:Destroy()
     cast.RayInfo.CosmeticBulletObject = nil
   end

   if self.BaseCast then
     self.BaseCast:TerminateCast(cast, castTerminatingFunction)
   end

-  if castTerminatingFunction then
-    castTerminatingFunction(cast)
-  end
-
-  self.Output:Fire("CastTerminating", cast)
 end

Fix 4 — src/FastCast2/init.luau: FastCastSerial:SyncChangesToCast references nonexistent field

BaseCastSerial has no SyncChange field. For serial mode, state mutations are immediate and no sync step is needed. Replace with a no-op:

 function FastCastSerial:SyncChangesToCast(cast: vaildcast)
-  if self.BaseCast.SyncChange then
-    self.BaseCast.SyncChange:Fire(cast)
-  end
+  -- Serial simulation applies mutations directly; no sync dispatch needed.
 end

Fix 5 — src/FastCast2/BaseCastSerial.luau: Remove unused Data parameter

Data is only read in a dead no-op block. Remove it and update the call site in init.luau.

In BaseCastSerial.luau:

-function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any)
+function BaseCastSerial.Init(BindableOutput: BindableEvent, parentCaster: any)
   local self = setmetatable({}, BaseCastSerial)
   self.Output = BindableOutput
   self.ParentCaster = parentCaster
   self.ObjectCache = nil
   self.BulkMoveToConnection = nil
   self.NextProjectileID = 0

-  if Data.useBulkMoveTo then
-    -- BulkMoveTo is handled by SerialSimulation
-  end

   return self
 end

In init.luau, update the Init call:

-self.BaseCast = BaseCastSerial.Init(BindableOutput, data, self)
+self.BaseCast = BaseCastSerial.Init(BindableOutput, self)

Fix 6 — src/FastCast2/BaseCastSerial.luau: Raycast method uses self.self.*

BaseCastSerial:Raycast still references self.self.ParentCaster and self.self.OutputBlockcast and Spherecast are already correct. Fix Raycast:

-  local cast = ActiveCastSerial.new(self.self.ParentCaster, castData)
+  local cast = ActiveCastSerial.new(self.ParentCaster, castData)
   SerialSimulation.Register(cast)

-  if self.self.Output then
-    self.self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior)
+  if self.Output then
+    self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior)
   end

Fix 7 — src/FastCast2/ActiveCastSerial.luau: Replace opaque castData table with explicit parameters

ActiveCastSerial.new(caster, castData) should use explicit named parameters like ActiveCast.createCastData does, not a raw untyped table. Also remove all the dead code defined in this file that belongs in SerialSimulation (the simulation functions, debug helpers, and constants are all unused here).

Replace the entire ActiveCastSerial.luau content with:

--[[
	- Author : Mawin CK
	- Date : 2025

	ActiveCastSerial - Constructs the cast data object for serial simulation.
]]

local FastCastModule = script.Parent
local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums"))

local CastVariantTypes = {
	[FastCastEnums.CastType.Raycast] = "Raycast",
	[FastCastEnums.CastType.Blockcast] = "Blockcast",
	[FastCastEnums.CastType.Spherecast] = "Spherecast",
}

local DEFAULT_MAX_DISTANCE = 1000

local function CloneCastParams(params: RaycastParams): RaycastParams
	local clone: RaycastParams = RaycastParams.new()
	clone.CollisionGroup = params.CollisionGroup
	clone.FilterType = params.FilterType
	clone.FilterDescendantsInstances = params.FilterDescendantsInstances
	clone.IgnoreWater = params.IgnoreWater
	return clone
end

local ActiveCastSerial = {}

--[=[
	`@function` new
	`@within` ActiveCastSerial

	`@param` caster any -- The parent Caster instance.
	`@param` id number -- The unique projectile ID.
	`@param` origin Vector3 -- Starting position.
	`@param` velocity Vector3 -- Initial velocity vector.
	`@param` acceleration Vector3 -- Acceleration vector.
	`@param` castType number -- FastCastEnums.CastType value.
	`@param` raycastParams RaycastParams? -- Optional raycast params (cloned if provided).
	`@param` maxDistance number? -- Max travel distance (defaults to 1000).
	`@param` cosmeticBullet Instance? -- Optional cosmetic bullet object.
	`@param` movementMethod string? -- "BulkMoveTo" or "Transform" (defaults to "BulkMoveTo").
	`@param` size Vector3? -- Block cast size (Blockcast only).
	`@param` radius number? -- Sphere cast radius (Spherecast only).
	`@param` highFidelitySegmentSize number? -- High-fidelity segment size.
	`@param` highFidelityBehavior number? -- High-fidelity behavior enum.
	`@param` visualizeCasts boolean? -- Whether to visualize casts.
	`@param` visualizeCastSettings any? -- Settings for cast visualization.
]=]
function ActiveCastSerial.new(
	caster: any,
	id: number,
	origin: Vector3,
	velocity: Vector3,
	acceleration: Vector3,
	castType: number,
	raycastParams: RaycastParams?,
	maxDistance: number?,
	cosmeticBullet: Instance?,
	movementMethod: string?,
	size: Vector3?,
	radius: number?,
	highFidelitySegmentSize: number?,
	highFidelityBehavior: number?,
	visualizeCasts: boolean?,
	visualizeCastSettings: any?
): any
	return {
		Caster = caster,
		StateInfo = {
			Paused = false,
			TotalRuntime = 0,
			DistanceCovered = 0,
			HighFidelitySegmentSize = highFidelitySegmentSize,
			HighFidelityBehavior = highFidelityBehavior,
			IsActivelyResimulating = false,
			CancelHighResCast = false,
			Trajectory = {
				StartTime = 0,
				EndTime = -1,
				Origin = origin,
				InitialVelocity = velocity,
				Acceleration = acceleration,
			},
			VisualizeCasts = visualizeCasts,
			VisualizeCastSettings = visualizeCastSettings,
		},
		RayInfo = {
			Parameters = raycastParams and CloneCastParams(raycastParams) or RaycastParams.new(),
			WorldRoot = workspace,
			MaxDistance = maxDistance or DEFAULT_MAX_DISTANCE,
			CosmeticBulletObject = cosmeticBullet,
			MovementMethod = movementMethod or "BulkMoveTo",
			Size = size,
			Radius = radius,
		},
		Type = CastVariantTypes[castType],
		CFrame = CFrame.new(origin),
		ID = id,
	}
end

return ActiveCastSerial

Fix 8 — src/FastCast2/BaseCastSerial.luau: Update all three cast methods to use explicit ActiveCastSerial.new parameters

After Fix 7, all three Raycast, Blockcast, and Spherecast methods must pass explicit arguments instead of a castData table. Replace the ActiveCastSerial.new(...) call in each:

In Raycast:

-  local castData = {
-    ID = self.NextProjectileID,
-    Origin = Origin,
-    Velocity = Velocity,
-    Acceleration = Behavior.Acceleration,
-    RaycastParams = raycastParams,
-    MaxDistance = Behavior.MaxDistance or 1000,
-    CosmeticBulletObject = cosmeticBullet,
-    CastType = EnumCastTypes.Raycast,
-    VisualizeCasts = Behavior.VisualizeCasts,
-    VisualizeCastSettings = Behavior.VisualizeCastSettings,
-    HighFidelitySegmentSize = Behavior.HighFidelitySegmentSize,
-    HighFidelityBehavior = Behavior.HighFidelityBehavior,
-    MovementMethod = Behavior.MovementMethod or "BulkMoveTo"
-  }
-  local cast = ActiveCastSerial.new(self.ParentCaster, castData)
+  local cast = ActiveCastSerial.new(
+    self.ParentCaster,
+    self.NextProjectileID,
+    Origin, Velocity,
+    Behavior.Acceleration or Vector3.zero,
+    EnumCastTypes.Raycast,
+    raycastParams,
+    Behavior.MaxDistance,
+    cosmeticBullet,
+    Behavior.MovementMethod,
+    nil, nil,
+    Behavior.HighFidelitySegmentSize,
+    Behavior.HighFidelityBehavior,
+    Behavior.VisualizeCasts,
+    Behavior.VisualizeCastSettings
+  )

In Blockcast:

-  local cast = ActiveCastSerial.new(self.ParentCaster, castData)
+  local cast = ActiveCastSerial.new(
+    self.ParentCaster,
+    self.NextProjectileID,
+    Origin, Velocity,
+    Behavior.Acceleration or Vector3.zero,
+    EnumCastTypes.Blockcast,
+    raycastParams,
+    Behavior.MaxDistance,
+    cosmeticBullet,
+    Behavior.MovementMethod,
+    Size, nil,
+    Behavior.HighFidelitySegmentSize,
+    Behavior.HighFidelityBehavior,
+    Behavior.VisualizeCasts,
+    Behavior.VisualizeCastSettings
+  )

In Spherecast:

-  local cast = ActiveCastSerial.new(self.ParentCaster, castData)
+  local cast = ActiveCastSerial.new(
+    self.ParentCaster,
+    self.NextProjectileID,
+    Origin, Velocity,
+    Behavior.Acceleration or Vector3.zero,
+    EnumCastTypes.Spherecast,
+    raycastParams,
+    Behavior.MaxDistance,
+    cosmeticBullet,
+    Behavior.MovementMethod,
+    nil, Radius,
+    Behavior.HighFidelitySegmentSize,
+    Behavior.HighFidelityBehavior,
+    Behavior.VisualizeCasts,
+    Behavior.VisualizeCastSettings
+  )

Fix 9 — src/FastCast2/SerialSimulation.luau: Remove all unused constants and dead code

Remove the following lines entirely (lines 21–32 and related unused requires):

-local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions"))
-local Configs = require(FastCastModule:WaitForChild("Configs"))
-local DebugLogging = Configs.DebugLogging
-local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial"))

-local HIGH_FIDE_INCREASE_SIZE = 0.5
-local MAX_SEGMENT_CAL_TIME = 0.016 * 5
-local MAX_CASTING_TIME = 0.2
-local DEFAULT_MAX_DISTANCE = 1000
-local EPSILON = 1e-6
-local RAY_SEARCH_OFFSET = 0.001

-local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059)
-local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765)
-local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1)
-local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235)

Remove the unused QueueEvent function:

-local function QueueEvent(callback: any, ...)
-  if callback then
-    table.insert(QueuedEvents, { Callback = callback, Args = { ... } })
-  end
-end

Fix 10 — src/FastCast2/ParallelSimulation.luau: Same removals as Fix 9

Apply identical removals: TypeDef, Configs/DebugLogging, and all unused constants (lines 19–30), and the unused QueueEvent function.


Fix 11 — src/FastCast2/SerialSimulation.luau & ParallelSimulation.luau: Unregister clears wrong castCFrame slot

In both Unregister functions, after swapping slot idlastId, the cleanup incorrectly clears castCFrame[id] instead of castCFrame[lastId]:

-  castCFrame[id] = nil   -- wrong: id now holds the swapped-in cast
+  castCFrame[lastId] = nil

Fix 12 — src/FastCast2/SerialSimulation.luau: Motor6D disconnect happens after swap

After the swap-with-last in Unregister, castMotor6D[id] already holds the swapped-in (kept) motor6D, not the one belonging to the removed cast. Save it before swapping:

 function SerialSimulation.Unregister(id: number)
   if not casts[id] then return end
+  local motorToDisconnect = castMotor6D[id]
   local lastId = castCount

   if id ~= lastId then
     ...
     castMotor6D[id] = castMotor6D[lastId]
     ...
   end

-  if castMotor6D[id] then
-    Motor6DPool.Disconnect(castMotor6D[id])
-  end
+  if motorToDisconnect then
+    Motor6DPool.Disconnect(motorToDisconnect)
+  end
   ...
 end

Apply the same pattern to ParallelSimulation.Unregister, replacing its current incorrect guard:

-  if id ~= lastId and castMotor6D[lastId] then
-    Motor6DPool.Disconnect(castMotor6D[lastId])
-  end

with the same save-before-swap approach.


Fix 13 — Both simulation files: CastHandler receives a table instead of raw value

In UpdateCasts in both SerialSimulation.luau and ParallelSimulation.luau, variant is a table { Size = ... } or { Radius = ... } but the castHandlers expect a raw Vector3 or number. Pass the value directly:

-  local variant = {}
-  if castType == EnumCastTypes.Blockcast then
-    variant.Size = castSize[i]
-  elseif castType == EnumCastTypes.Spherecast then
-    variant.Radius = castRadius[i]
-  end
-  local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], variant)
+  local sizeOrRadius: (Vector3 | number)?
+  if castType == EnumCastTypes.Blockcast then
+    sizeOrRadius = castSize[i]
+  elseif castType == EnumCastTypes.Spherecast then
+    sizeOrRadius = castRadius[i]
+  end
+  local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], sizeOrRadius)

@weenachuangkud
Copy link
Copy Markdown
Owner Author

FastCast2 AI Agent Prompt — Cleanup & Bug Fixes

Fix all of the following issues across src/FastCast2. Apply every change exactly as described.

Fix 1 — src/FastCast2/init.luau: FastCastSerial.Behavior and FastCastParallel.newBehavior should simpify to FastCast.newBehavior

- function FastCastSerial.newBehavior() ... end
- function FastCastParallel.newBehavior ... end
+ function FastCast.newBehavior() ... end

Fix 2 — src/FastCast2/init.luau: Remove stale UpdateConnection guards from FastCastParallel methods

The SoA parallel simulation never sets UpdateConnection on any cast. Every assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED") in FastCastParallel will always error. Remove that assert from every method that has it: GetVelocityCast, GetAccelerationCast, GetPositionCast, SetVelocityCast, SetAccelerationCast, SetPositionCast, PauseCast, AddPositionCast, AddVelocityCast, AddAccelerationCast, SyncChangesToCast:

 function FastCastParallel:GetVelocityCast(cast: vaildcast)
-  assert(cast.StateInfo.UpdateConnection ~= nil, "ERR_OBJECT_DISPOSED")
   local currentTrajectory = cast.StateInfo.Trajectory
   ...
 end
 -- (repeat for every other method listed above)

Fix 3 — src/FastCast2/init.luau: FastCastSerial:TerminateCast double-fires and double-calls

FastCastSerial:TerminateCast calls self.BaseCast:TerminateCast(cast, castTerminatingFunction), which already fires Output:Fire("CastTerminating", cast) and calls castTerminatingFunction(cast). Then it calls both again directly, causing double emission and a double callback. Remove the redundant direct calls:

 function FastCastSerial:TerminateCast(cast: vaildcast, castTerminatingFunction: TypeDef.OnCastTerminatingFunction?)
   if cast.StateInfo.UpdateConnection ~= nil then
     cast.StateInfo.UpdateConnection:Disconnect()
     cast.StateInfo.UpdateConnection = nil
   end

   if cast.RayInfo.CosmeticBulletObject then
     cast.RayInfo.CosmeticBulletObject:Destroy()
     cast.RayInfo.CosmeticBulletObject = nil
   end

   if self.BaseCast then
     self.BaseCast:TerminateCast(cast, castTerminatingFunction)
   end

-  if castTerminatingFunction then
-    castTerminatingFunction(cast)
-  end
-
-  self.Output:Fire("CastTerminating", cast)
 end

Fix 4 — src/FastCast2/init.luau: FastCastSerial:SyncChangesToCast references nonexistent field

BaseCastSerial has no SyncChange field. For serial mode, state mutations are immediate and no sync step is needed. Replace with a no-op:

 function FastCastSerial:SyncChangesToCast(cast: vaildcast)
-  if self.BaseCast.SyncChange then
-    self.BaseCast.SyncChange:Fire(cast)
-  end
+  -- Serial simulation applies mutations directly; no sync dispatch needed.
 end

Fix 5 — src/FastCast2/BaseCastSerial.luau: Remove unused Data parameter

Data is only read in a dead no-op block. Remove it and update the call site in init.luau.

In BaseCastSerial.luau:

-function BaseCastSerial.Init(BindableOutput: BindableEvent, Data: any, parentCaster: any)
+function BaseCastSerial.Init(BindableOutput: BindableEvent, parentCaster: any)
   local self = setmetatable({}, BaseCastSerial)
   self.Output = BindableOutput
   self.ParentCaster = parentCaster
   self.ObjectCache = nil
   self.BulkMoveToConnection = nil
   self.NextProjectileID = 0

-  if Data.useBulkMoveTo then
-    -- BulkMoveTo is handled by SerialSimulation
-  end

   return self
 end

In init.luau, update the Init call:

-self.BaseCast = BaseCastSerial.Init(BindableOutput, data, self)
+self.BaseCast = BaseCastSerial.Init(BindableOutput, self)

Fix 6 — src/FastCast2/BaseCastSerial.luau: Raycast method uses self.self.*

BaseCastSerial:Raycast still references self.self.ParentCaster and self.self.OutputBlockcast and Spherecast are already correct. Fix Raycast:

-  local cast = ActiveCastSerial.new(self.self.ParentCaster, castData)
+  local cast = ActiveCastSerial.new(self.ParentCaster, castData)
   SerialSimulation.Register(cast)

-  if self.self.Output then
-    self.self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior)
+  if self.Output then
+    self.Output:Fire("CastFire", cast, Origin, Direction, Velocity, Behavior)
   end

Fix 7 — src/FastCast2/ActiveCastSerial.luau: Replace opaque castData table with explicit parameters

ActiveCastSerial.new(caster, castData) should use explicit named parameters like ActiveCast.createCastData does, not a raw untyped table. Also remove all the dead code defined in this file that belongs in SerialSimulation (the simulation functions, debug helpers, and constants are all unused here).

Replace the entire ActiveCastSerial.luau content with:

--[[
	- Author : Mawin CK
	- Date : 2025

	ActiveCastSerial - Constructs the cast data object for serial simulation.
]]

local FastCastModule = script.Parent
local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums"))

local CastVariantTypes = {
	[FastCastEnums.CastType.Raycast] = "Raycast",
	[FastCastEnums.CastType.Blockcast] = "Blockcast",
	[FastCastEnums.CastType.Spherecast] = "Spherecast",
}

local DEFAULT_MAX_DISTANCE = 1000

local function CloneCastParams(params: RaycastParams): RaycastParams
	local clone: RaycastParams = RaycastParams.new()
	clone.CollisionGroup = params.CollisionGroup
	clone.FilterType = params.FilterType
	clone.FilterDescendantsInstances = params.FilterDescendantsInstances
	clone.IgnoreWater = params.IgnoreWater
	return clone
end

local ActiveCastSerial = {}

function ActiveCastSerial.new(
	BaseCast: TypeDef.BaseCastData,
  activeCastID: number,
  origin: Vector3,
  direction: Vector3,
  velocity: Vector3 | number,
  behavior: TypeDef.FastCastBehavior,
  eventModule: TypeDef.FastCastEventsModule?,
  variant: CastVariants
): any
	...
end

Fix 8 — src/FastCast2/BaseCastSerial.luau: Update all three cast methods to use explicit ActiveCastSerial.new parameters

After Fix 7, all three Raycast, Blockcast, and Spherecast methods must pass explicit arguments instead of a castData table. Replace the ActiveCastSerial.new(...) call in each:

Fix 9 — src/FastCast2/SerialSimulation.luau: Remove all unused constants and dead code

Remove the following lines entirely (lines 21–32 and related unused requires):

-local TypeDef = require(FastCastModule:WaitForChild("TypeDefinitions"))
-local Configs = require(FastCastModule:WaitForChild("Configs"))
-local DebugLogging = Configs.DebugLogging
-local ActiveCastSerial = require(FastCastModule:WaitForChild("ActiveCastSerial"))

-local HIGH_FIDE_INCREASE_SIZE = 0.5
-local MAX_SEGMENT_CAL_TIME = 0.016 * 5
-local MAX_CASTING_TIME = 0.2
-local DEFAULT_MAX_DISTANCE = 1000
-local EPSILON = 1e-6
-local RAY_SEARCH_OFFSET = 0.001

-local DBG_SEGMENT_SUB_COLOR = Color3.new(0.286275, 0.329412, 0.247059)
-local DBG_SEGMENT_SUB_COLOR2 = Color3.new(0.454902, 0.933333, 0.011765)
-local DBG_HIT_SUB_COLOR = Color3.new(0.0588235, 0.87451, 1)
-local DBG_RAYPIERCE_SUB_COLOR = Color3.new(1, 0.113725, 0.588235)

Remove the unused QueueEvent function:

-local function QueueEvent(callback: any, ...)
-  if callback then
-    table.insert(QueuedEvents, { Callback = callback, Args = { ... } })
-  end
-end

Fix 10 — src/FastCast2/ParallelSimulation.luau: Same removals as Fix 9

Apply identical removals: TypeDef, Configs/DebugLogging, and all unused constants (lines 19–30), and the unused QueueEvent function.

Fix 11 — src/FastCast2/SerialSimulation.luau & ParallelSimulation.luau: Unregister clears wrong castCFrame slot

In both Unregister functions, after swapping slot idlastId, the cleanup incorrectly clears castCFrame[id] instead of castCFrame[lastId]:

-  castCFrame[id] = nil   -- wrong: id now holds the swapped-in cast
+  castCFrame[lastId] = nil

Fix 12 — src/FastCast2/SerialSimulation.luau: Motor6D disconnect happens after swap

After the swap-with-last in Unregister, castMotor6D[id] already holds the swapped-in (kept) motor6D, not the one belonging to the removed cast. Save it before swapping:

 function SerialSimulation.Unregister(id: number)
   if not casts[id] then return end
+  local motorToDisconnect = castMotor6D[id]
   local lastId = castCount

   if id ~= lastId then
     ...
     castMotor6D[id] = castMotor6D[lastId]
     ...
   end

-  if castMotor6D[id] then
-    Motor6DPool.Disconnect(castMotor6D[id])
-  end
+  if motorToDisconnect then
+    Motor6DPool.Disconnect(motorToDisconnect)
+  end
   ...
 end

Apply the same pattern to ParallelSimulation.Unregister, replacing its current incorrect guard:

-  if id ~= lastId and castMotor6D[lastId] then
-    Motor6DPool.Disconnect(castMotor6D[lastId])
-  end

with the same save-before-swap approach.

Fix 13 — Both simulation files: CastHandler receives a table instead of raw value

In UpdateCasts in both SerialSimulation.luau and ParallelSimulation.luau, variant is a table { Size = ... } or { Radius = ... } but the castHandlers expect a raw Vector3 or number. Pass the value directly:

-  local variant = {}
-  if castType == EnumCastTypes.Blockcast then
-    variant.Size = castSize[i]
-  elseif castType == EnumCastTypes.Spherecast then
-    variant.Radius = castRadius[i]
-  end
-  local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], variant)
+  local sizeOrRadius: (Vector3 | number)?
+  if castType == EnumCastTypes.Blockcast then
+    sizeOrRadius = castSize[i]
+  elseif castType == EnumCastTypes.Spherecast then
+    sizeOrRadius = castRadius[i]
+  end
+  local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], sizeOrRadius)

Repository owner deleted a comment from coderabbitai Bot May 8, 2026
Repository owner deleted a comment from coderabbitai Bot May 8, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== ActiveCast.luau - full SimulateCast and high fidelity section ==="
cat src/FastCast2/ActiveCast.luau

echo ""
echo "=== SerialSimulation.luau UpdateCasts ==="
grep -n "UpdateCasts\|HighFidelity\|highFidelity\|HighFide\|subcast\|SubCast\|subRay\|Stepped\|Always\|Once\|SegmentSize\|fidelity\|hifi\|resim\|CancelHighRes\|IsActivelyResim" src/FastCast2/SerialSimulation.luau

echo ""
echo "=== ParallelSimulation.luau UpdateCasts ==="
grep -n "UpdateCasts\|HighFidelity\|highFidelity\|HighFide\|subcast\|SubCast\|subRay\|Stepped\|Always\|Once\|SegmentSize\|fidelity\|hifi\|resim\|CancelHighRes\|IsActivelyResim" src/FastCast2/ParallelSimulation.luau

echo ""
echo "=== SerialSimulation.luau full UpdateCasts function ==="
awk '/function.*UpdateCasts/,/^end$/' src/FastCast2/SerialSimulation.luau

echo ""
echo "=== ParallelSimulation.luau full UpdateCasts function ==="
awk '/function.*UpdateCasts/,/^end$/' src/FastCast2/ParallelSimulation.luau

echo ""
echo "=== FastCastEnums.luau HighFidelityBehavior values ==="
cat src/FastCast2/FastCastEnums.luau

echo ""
echo "=== TypeDefinitions.luau CastStateInfo ==="
grep -n -A 20 "CastStateInfo\|HighFidelity" src/FastCast2/TypeDefinitions.luau

Length of output: 45265


@weenachuangkud Here is the focused AI agent prompt:


FastCast2 AI Agent Prompt — Implement HighFidelitySegmentSize & HighFidelityBehavior in SoA Simulations

HighFidelitySegmentSize and HighFidelityBehavior are already stored in SoA arrays in both SerialSimulation.luau and ParallelSimulation.luau (populated in Register/Unregister) but are never read in UpdateCasts. Implement the two high-fidelity modes (Automatic = 2, Always = 3) by replacing the inner loop body in UpdateCasts in both files identically (except the final Terminate call uses SerialSimulation.Terminate vs ParallelSimulation.Terminate).

⚠️ Do not touch ActiveCast.luau — it is the parallel per-cast simulation and already has its own high-fidelity implementation.


Background: how high-fidelity modes work (from ActiveCast.luau)

Enum Behaviour
Default = 1 No subdivision — current behaviour, one cast per frame
Automatic = 2 One coarse cast per frame; if a hit is detected, subdivide the displacement into floor(rayDisplacement / HighFidelitySegmentSize) sub-segments and re-cast each one to find the precise hit point
Always = 3 Every frame, estimate the frame displacement, subdivide the full deltaTime into floor(displacement / HighFidelitySegmentSize) sub-segments, and simulate each one individually

Both modes share these details:

  • numSegmentsReal = max(1, floor(rayDisplacement / segmentSize))
  • timeIncrement = deltaTime / numSegmentsReal
  • A castIsActivelyResimulating[i] guard prevents cascading lag — if already resimulating when re-entered, terminate + warn.
  • A castCancelHighResCast[i] flag can break out of the sub-loop early (set externally, e.g. by SyncChangesToCast).

Shared helpers needed (add near the top of each file, alongside GetPositionAtTime/GetVelocityAtTime)

Both files already have GetPositionAtTime and GetVelocityAtTime. No new helpers are needed — the sub-loop inlines the same kinematics.


Fix — Replace UpdateCasts inner loop body in both simulation files

Replace the existing per-cast block inside the for i = 1, castCount do loop with the following. The only difference between the two files is the Terminate call at the end (SerialSimulation.Terminate vs ParallelSimulation.Terminate). The template below uses ThisSimulation as a placeholder — substitute accordingly.

local function UpdateCasts(deltaTime: number)
    if castCount == 0 then return end

    local destroyedIds = {} :: { number }

    for i = 1, castCount do
        if castPaused[i] then continue end

        local caster = castCaster[i]
        local castType = castCastType[i]
        local CastHandler = castHandlers[castType]

        local origin = castOrigin[i]
        local velocity = castVelocity[i]
        local acceleration = castAcceleration[i]

        -- Resolve the raw size/radius value (not a table)
        local sizeOrRadius: (Vector3 | number)?
        if castType == EnumCastTypes.Blockcast then
            sizeOrRadius = castSize[i]
        elseif castType == EnumCastTypes.Spherecast then
            sizeOrRadius = castRadius[i]
        end

        local hifiBehavior = castHighFidelityBehavior[i]
        local hifiSegmentSize = castHighFidelitySegmentSize[i]
        local bullet = castCosmeticBullet[i]
        local motor6d = castMotor6D[i]

        -- ──────────────────────────────────────────────────────────────
        -- HighFidelityBehavior.Always (3):
        -- Subdivide every frame into sub-segments before casting.
        -- ──────────────────────────────────────────────────────────────
        if hifiBehavior == 3 and hifiSegmentSize > 0 then

            -- Cascading lag guard
            if castIsActivelyResimulating[i] then
                table.insert(destroyedIds, i)
                warn("Cascading cast lag encountered on cast " .. i .. "! Consider increasing HighFidelitySegmentSize.")
                continue
            end
            castIsActivelyResimulating[i] = true

            -- Estimate frame displacement to compute segment count
            local totalDeltaForEstimate = castTotalRuntime[i]
            local posNow   = GetPositionAtTime(totalDeltaForEstimate, origin, velocity, acceleration)
            local velNow   = GetVelocityAtTime(totalDeltaForEstimate, velocity, acceleration)
            local posNext  = GetPositionAtTime(totalDeltaForEstimate + deltaTime, origin, velocity, acceleration)
            local estimatedDisplacement = (posNext - posNow).Magnitude

            -- Estimate via coarse cast to see if we even need subdivision
            local coarseRayDir = (posNext - posNow).Unit * velNow.Magnitude * deltaTime
            local coarseResult = CastHandler(castWorldRoot[i], posNow, coarseRayDir, castRaycastParams[i], sizeOrRadius)
            if coarseResult then
                estimatedDisplacement = (coarseResult.Position - posNow).Magnitude
            end

            local numSegmentsReal = math.max(1, math.floor(estimatedDisplacement / hifiSegmentSize))
            local timeIncrement = deltaTime / numSegmentsReal

            local hitScheduled = false

            for segIndex = 1, numSegmentsReal do
                if castCancelHighResCast[i] then
                    castCancelHighResCast[i] = false
                    break
                end

                local preDelta  = castTotalRuntime[i]
                local lastPoint = GetPositionAtTime(preDelta, origin, velocity, acceleration)

                castTotalRuntime[i] += timeIncrement
                local nowDelta      = castTotalRuntime[i]

                local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
                local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
                local displacement    = currentPosition - lastPoint
                local rayDir          = displacement.Unit * currentVelocity.Magnitude * timeIncrement

                local result = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)

                local hitPoint = currentPosition
                local hitPart  = nil
                if result then
                    hitPoint = result.Position
                    hitPart  = result.Instance
                end

                local rayDisplacement = (hitPoint - lastPoint).Magnitude
                castDistanceCovered[i] += rayDisplacement

                local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
                castCFrame[i] = newCFrame

                -- Move cosmetic bullet
                if bullet then
                    if motor6d then
                        motor6d.Transform = newCFrame
                    elseif bullet:IsA("BasePart") then
                        bullet.CFrame = newCFrame
                    else
                        bullet:PivotTo(newCFrame)
                    end
                end

                QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)

                if result and hitPart ~= bullet then
                    QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
                    hitScheduled = true
                    break
                end

                if castDistanceCovered[i] >= castMaxDistance[i] then
                    hitScheduled = true
                    break
                end
            end

            castIsActivelyResimulating[i] = false

            if hitScheduled then
                table.insert(destroyedIds, i)
            end

        -- ──────────────────────────────────────────────────────────────
        -- HighFidelityBehavior.Automatic (2):
        -- Do one coarse cast; on hit, subdivide for precision.
        -- ──────────────────────────────────────────────────────────────
        elseif hifiBehavior == 2 and hifiSegmentSize > 0 then

            -- Coarse cast (same as Default)
            local preDelta    = castTotalRuntime[i]
            local lastPoint   = GetPositionAtTime(preDelta, origin, velocity, acceleration)

            castTotalRuntime[i] += deltaTime
            local nowDelta    = castTotalRuntime[i]

            local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
            local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
            local displacement    = currentPosition - lastPoint
            local rayDir          = displacement.Unit * currentVelocity.Magnitude * deltaTime

            local result  = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)

            local hitPoint = currentPosition
            local hitPart  = nil
            if result then
                hitPoint = result.Position
                hitPart  = result.Instance
            end

            local rayDisplacement = (hitPoint - lastPoint).Magnitude
            castDistanceCovered[i] += rayDisplacement

            local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
            castCFrame[i] = newCFrame

            if bullet then
                if motor6d then
                    motor6d.Transform = newCFrame
                elseif bullet:IsA("BasePart") then
                    bullet.CFrame = newCFrame
                else
                    bullet:PivotTo(newCFrame)
                end
            end

            QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)

            -- On coarse hit → refine with sub-segments
            if result and hitPart ~= bullet then

                -- Cascading lag guard
                if castIsActivelyResimulating[i] then
                    QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
                    table.insert(destroyedIds, i)
                    warn("Cascading cast lag encountered on cast " .. i .. "! Consider increasing HighFidelitySegmentSize.")
                    continue
                end
                castIsActivelyResimulating[i] = true

                local numSegmentsReal = math.max(1, math.floor(rayDisplacement / hifiSegmentSize))
                local timeIncrement   = deltaTime / numSegmentsReal

                -- Rewind TotalRuntime to re-simulate sub-segments from preDelta
                castTotalRuntime[i] = preDelta
                -- Also rewind distance contribution for re-counting
                castDistanceCovered[i] -= rayDisplacement

                local preciseHit = false

                for segIndex = 1, numSegmentsReal do
                    if castCancelHighResCast[i] then
                        castCancelHighResCast[i] = false
                        break
                    end

                    local subPreDelta  = castTotalRuntime[i]
                    local subLastPoint = GetPositionAtTime(subPreDelta, origin, velocity, acceleration)

                    castTotalRuntime[i] += timeIncrement
                    local subNowDelta   = castTotalRuntime[i]

                    local subCurrentPosition = GetPositionAtTime(subNowDelta, origin, velocity, acceleration)
                    local subCurrentVelocity = GetVelocityAtTime(subNowDelta, velocity, acceleration)
                    local subDisplacement    = subCurrentPosition - subLastPoint
                    local subRayDir          = subCurrentVelocity * timeIncrement

                    local subResult = CastHandler(castWorldRoot[i], subLastPoint, subRayDir, castRaycastParams[i], sizeOrRadius)

                    local subHitPoint = subCurrentPosition
                    local subHitPart  = nil
                    if subResult then
                        subHitPoint = subResult.Position
                        subHitPart  = subResult.Instance
                    end

                    local subRayDisplacement = (subHitPoint - subLastPoint).Magnitude
                    castDistanceCovered[i] += subRayDisplacement

                    local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subRayDisplacement / 2)
                    castCFrame[i] = subCFrame

                    if bullet then
                        if motor6d then
                            motor6d.Transform = subCFrame
                        elseif bullet:IsA("BasePart") then
                            bullet.CFrame = subCFrame
                        else
                            bullet:PivotTo(subCFrame)
                        end
                    end

                    if subResult and subHitPart ~= bullet then
                        QueueFire(caster, "Hit", casts[i], subResult, subCurrentVelocity, bullet)
                        preciseHit = true
                        break
                    end

                    if castDistanceCovered[i] >= castMaxDistance[i] then
                        preciseHit = true
                        break
                    end
                end

                castIsActivelyResimulating[i] = false

                -- If sub-loop found no hit (can pierce case / numeric edge), don't terminate
                if preciseHit then
                    table.insert(destroyedIds, i)
                end

            elseif castDistanceCovered[i] >= castMaxDistance[i] then
                table.insert(destroyedIds, i)
            end

        -- ──────────────────────────────────────────────────────────────
        -- HighFidelityBehavior.Default (1) — unchanged behaviour
        -- ──────────────────────────────────────────────────────────────
        else
            local preDelta  = castTotalRuntime[i]
            local lastPoint = GetPositionAtTime(preDelta, origin, velocity, acceleration)

            castTotalRuntime[i] += deltaTime
            local nowDelta  = castTotalRuntime[i]

            local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
            local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
            local displacement    = currentPosition - lastPoint
            local rayDir          = displacement.Unit * currentVelocity.Magnitude * deltaTime

            local result = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)

            local hitPoint = currentPosition
            local hitPart  = nil
            if result then
                hitPoint = result.Position
                hitPart  = result.Instance
            end

            local rayDisplacement = (hitPoint - lastPoint).Magnitude
            castDistanceCovered[i] += rayDisplacement

            local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
            castCFrame[i] = newCFrame

            if bullet then
                if motor6d then
                    motor6d.Transform = newCFrame
                elseif bullet:IsA("BasePart") then
                    bullet.CFrame = newCFrame
                else
                    bullet:PivotTo(newCFrame)
                end
            end

            QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)

            if result and hitPart ~= bullet then
                QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
                table.insert(destroyedIds, i)
            elseif castDistanceCovered[i] >= castMaxDistance[i] then
                table.insert(destroyedIds, i)
            end
        end
    end

    -- Terminate in reverse to avoid swap-with-last index invalidation
    for i = `#destroyedIds`, 1, -1 do
        ThisSimulation.Terminate(destroyedIds[i])  -- replace with SerialSimulation or ParallelSimulation
    end

    DispatchAllEvents()
end

Summary of what changed vs. current UpdateCasts

Change Reason
Branch on castHighFidelityBehavior[i] Route to correct simulation mode
Always (3): estimate displacement → subdivide deltaTime → simulate each sub-segment Every-frame precision
Automatic (2): coarse cast → on hit, rewind TotalRuntime and DistanceCovered, then subdivide for precise hit On-demand precision only when needed
castIsActivelyResimulating[i] guard in both HiFi paths Prevents cascading lag
castCancelHighResCast[i] checked per sub-segment iteration Allows external early-exit
Raw sizeOrRadius passed to CastHandler (not a table) Fixes pre-existing bug (Fix 13)
Default (1): identical to current code No regression

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me read the simulation files and the existing high-fidelity logic in ActiveCast.luau first.


🏁 Script executed:

#!/bin/bash
echo "=== src/FastCast2/SerialSimulation.luau ==="
cat src/FastCast2/SerialSimulation.luau

echo ""
echo "=== src/FastCast2/ParallelSimulation.luau ==="
cat src/FastCast2/ParallelSimulation.luau

echo ""
echo "=== src/FastCast2/ActiveCast.luau (high-fidelity section) ==="
cat src/FastCast2/ActiveCast.luau

echo ""
echo "=== FastCastEnums.luau ==="
cat src/FastCast2/FastCastEnums.luau

Length of output: 54537


@weenachuangkud Here is the AI agent prompt:


FastCast2 AI Agent Prompt — Implement HighFidelitySegmentSize & HighFidelityBehavior in SerialSimulation and ParallelSimulation

Both src/FastCast2/SerialSimulation.luau and src/FastCast2/ParallelSimulation.luau already store castHighFidelitySegmentSize[id] and castHighFidelityBehavior[id] in their SoA arrays but never use them in UpdateCasts. Apply all changes below identically to both files.

Reference the existing implementation in src/FastCast2/ActiveCast.luau for the correct sub-cast math. The three modes from FastCastEnums.HighFidelityBehavior are:

  • Default = 1 — single cast per frame (current behavior, no change).
  • Automatic = 2 — single cast per frame; on hit, subdivide the segment to find the exact impact point.
  • Always = 3 — every frame subdivide into N sub-casts regardless of hit.

Step 1 — Add the PerformHighFidelitySubcasts helper

Add this function before UpdateCasts in both files:

-- Performs N sub-casts over [runtimeBefore, runtimeBefore + deltaTime].
-- Returns true and schedules termination via destroyedIds if a hit is found.
-- Fires LengthChanged for every sub-segment.
local function PerformHighFidelitySubcasts(
	i: number,
	runtimeBefore: number,
	deltaTime: number,
	numSegments: number,
	origin: Vector3,
	velocity: Vector3,
	acceleration: Vector3,
	CastHandler: any,
	sizeOrRadius: (Vector3 | number)?,
	destroyedIds: { number }
): boolean
	local timeIncrement = deltaTime / numSegments
	local caster = castCaster[i]
	local bullet = castCosmeticBullet[i]
	local motor6d = castMotor6D[i]
	local hitFound = false

	for segIdx = 1, numSegments do
		if castCancelHighResCast[i] then
			castCancelHighResCast[i] = false
			break
		end

		local segT0 = runtimeBefore + timeIncrement * (segIdx - 1)
		local segT1 = runtimeBefore + timeIncrement * segIdx

		local segLastPos   = GetPositionAtTime(segT0, origin, velocity, acceleration)
		local segCurrPos   = GetPositionAtTime(segT1, origin, velocity, acceleration)
		local segVelocity  = GetVelocityAtTime(segT1, velocity, acceleration)

		-- Use velocity * timeIncrement to match ActiveCast.luau subRayDir fix
		local segRayDir = segVelocity * timeIncrement

		local segResult = CastHandler(castWorldRoot[i], segLastPos, segRayDir, castRaycastParams[i], sizeOrRadius)

		local segHitPoint = segCurrPos
		if segResult then
			segHitPoint = segResult.Position
		end

		local segDisplacement = (segHitPoint - segLastPos).Magnitude
		castDistanceCovered[i] += segDisplacement
		castTotalRuntime[i] = segT1

		local newCFrame = CFrame.new(segLastPos, segLastPos + segRayDir) * CFrame.new(0, 0, -segDisplacement / 2)
		castCFrame[i] = newCFrame

		if bullet then
			if motor6d then
				motor6d.Transform = newCFrame
			elseif bullet:IsA("BasePart") then
				bullet.CFrame = newCFrame
			else
				bullet:PivotTo(newCFrame)
			end
		end

		QueueFire(caster, "LengthChanged", casts[i], segLastPos, segRayDir.Unit, segDisplacement, segVelocity, bullet)

		if segResult and segResult.Instance ~= bullet then
			QueueFire(caster, "Hit", casts[i], segResult, segVelocity, bullet)
			table.insert(destroyedIds, i)
			hitFound = true
			break
		end

		if castDistanceCovered[i] >= castMaxDistance[i] then
			table.insert(destroyedIds, i)
			hitFound = true
			break
		end
	end

	return hitFound
end

Step 2 — Replace the UpdateCasts per-cast body

In UpdateCasts, replace everything inside the for i = 1, castCount do loop (after the continue guard and local variable declarations) with the following. The structure branches on castHighFidelityBehavior[i]:

	for i = 1, castCount do
		if castPaused[i] then continue end

		local caster    = castCaster[i]
		local castType  = castCastType[i]
		local CastHandler = castHandlers[castType]

		local origin       = castOrigin[i]
		local velocity     = castVelocity[i]
		local acceleration = castAcceleration[i]

		local sizeOrRadius: (Vector3 | number)?
		if castType == EnumCastTypes.Blockcast then
			sizeOrRadius = castSize[i]
		elseif castType == EnumCastTypes.Spherecast then
			sizeOrRadius = castRadius[i]
		end

		local hfBehavior    = castHighFidelityBehavior[i] or 1
		local hfSegmentSize = castHighFidelitySegmentSize[i] or 1

		local bullet = castCosmeticBullet[i]
		local motor6d = castMotor6D[i]

		-- ── Always mode (3): subdivide every frame ──────────────────────────────
		if hfBehavior == FastCastEnums.HighFidelityBehavior.Always and hfSegmentSize > 0 then
			if castIsActivelyResimulating[i] then
				-- Cascading lag guard: terminate to prevent exponential cost
				warn("FastCast2: Cascading cast lag on cast " .. tostring(castIDs[i]) .. ". Increase HighFidelitySegmentSize.")
				table.insert(destroyedIds, i)
				continue
			end

			castIsActivelyResimulating[i] = true

			local runtimeBefore = castTotalRuntime[i]

			-- Estimate displacement to determine segment count
			local prePos  = GetPositionAtTime(runtimeBefore, origin, velocity, acceleration)
			local postPos = GetPositionAtTime(runtimeBefore + deltaTime, origin, velocity, acceleration)
			local estDisplacement = (postPos - prePos).Magnitude

			local numSegments = math.max(1, math.floor(estDisplacement / hfSegmentSize))

			PerformHighFidelitySubcasts(
				i, runtimeBefore, deltaTime, numSegments,
				origin, velocity, acceleration,
				CastHandler, sizeOrRadius, destroyedIds
			)

			-- Ensure runtime is advanced even if no hit was found
			if castTotalRuntime[i] == runtimeBefore then
				castTotalRuntime[i] = runtimeBefore + deltaTime
			end

			castIsActivelyResimulating[i] = false

		-- ── Automatic mode (2): single cast, sub-cast only on hit ────────────────
		elseif hfBehavior == FastCastEnums.HighFidelityBehavior.Automatic and hfSegmentSize > 0 then
			local runtimeBefore = castTotalRuntime[i]
			local totalDelta    = runtimeBefore

			local lastPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration)
			castTotalRuntime[i] += deltaTime
			totalDelta = castTotalRuntime[i]

			local currentPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration)
			local currentVelocity = GetVelocityAtTime(totalDelta, velocity, acceleration)
			local displacement    = currentPosition - lastPosition
			local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime

			local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], sizeOrRadius)

			if result and result.Instance ~= bullet then
				-- Hit detected — resimulate to find exact point
				if castIsActivelyResimulating[i] then
					warn("FastCast2: Cascading cast lag on cast " .. tostring(castIDs[i]) .. ". Increase HighFidelitySegmentSize.")
					table.insert(destroyedIds, i)
					continue
				end

				castIsActivelyResimulating[i] = true

				local rayDisplacement = (result.Position - lastPosition).Magnitude
				local numSegments = math.max(1, math.floor(rayDisplacement / hfSegmentSize))

				-- Rewind time and let PerformHighFidelitySubcasts advance it
				castTotalRuntime[i] = runtimeBefore
				castDistanceCovered[i] -= 0  -- distance not yet added in this branch

				local hitFound = PerformHighFidelitySubcasts(
					i, runtimeBefore, deltaTime, numSegments,
					origin, velocity, acceleration,
					CastHandler, sizeOrRadius, destroyedIds
				)

				if not hitFound then
					-- Sub-cast found no hit (can happen with curved paths); fire normal LengthChanged
					local hitPoint = result.Position
					local rayDisp2 = (hitPoint - lastPosition).Magnitude
					castDistanceCovered[i] += rayDisp2
					local newCFrame = CFrame.new(lastPosition, lastPosition + rayDir) * CFrame.new(0, 0, -rayDisp2 / 2)
					castCFrame[i] = newCFrame
					if bullet then
						if motor6d then motor6d.Transform = newCFrame
						elseif bullet:IsA("BasePart") then bullet.CFrame = newCFrame
						else bullet:PivotTo(newCFrame) end
					end
					QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisp2, currentVelocity, bullet)
				end

				castIsActivelyResimulating[i] = false
			else
				-- No hit — normal update
				local hitPoint = currentPosition
				if result then hitPoint = result.Position end
				local rayDisplacement = (hitPoint - lastPosition).Magnitude
				castDistanceCovered[i] += rayDisplacement

				local newCFrame = CFrame.new(lastPosition, lastPosition + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
				castCFrame[i] = newCFrame
				if bullet then
					if motor6d then motor6d.Transform = newCFrame
					elseif bullet:IsA("BasePart") then bullet.CFrame = newCFrame
					else bullet:PivotTo(newCFrame) end
				end

				QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet)

				if castDistanceCovered[i] >= castMaxDistance[i] then
					table.insert(destroyedIds, i)
				end
			end

		-- ── Default mode (1): single cast per frame (existing behavior) ──────────
		else
			local totalDelta = castTotalRuntime[i]

			local lastPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration)
			castTotalRuntime[i] += deltaTime
			totalDelta = castTotalRuntime[i]

			local currentPosition = GetPositionAtTime(totalDelta, origin, velocity, acceleration)
			local currentVelocity = GetVelocityAtTime(totalDelta, velocity, acceleration)
			local displacement    = currentPosition - lastPosition
			local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime

			local result = CastHandler(castWorldRoot[i], lastPosition, rayDir, castRaycastParams[i], sizeOrRadius)

			local hitPoint = currentPosition
			local hitPart  = nil
			if result then
				hitPoint = result.Position
				hitPart  = result.Instance
			end

			local rayDisplacement = (hitPoint - lastPosition).Magnitude
			castDistanceCovered[i] += rayDisplacement

			local newCFrame = CFrame.new(lastPosition, lastPosition + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
			castCFrame[i] = newCFrame

			if bullet then
				if motor6d then motor6d.Transform = newCFrame
				elseif bullet:IsA("BasePart") then bullet.CFrame = newCFrame
				else bullet:PivotTo(newCFrame) end
			end

			QueueFire(caster, "LengthChanged", casts[i], lastPosition, rayDir.Unit, rayDisplacement, currentVelocity, bullet)

			if result and hitPart ~= bullet then
				QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
				table.insert(destroyedIds, i)
			end

			if castDistanceCovered[i] >= castMaxDistance[i] then
				table.insert(destroyedIds, i)
			end
		end
	end

Step 3 — Add FastCastEnums require to both files (if not already present)

FastCastEnums is already required in both files under local FastCastEnums = require(FastCastModule:WaitForChild("FastCastEnums")). No change needed here, but confirm EnumCastTypes and FastCastEnums.HighFidelityBehavior are both accessible. They are — EnumCastTypes = FastCastEnums.CastType is already defined, and FastCastEnums.HighFidelityBehavior is used directly in the new code above.


Notes on correctness

Concern Handled
subRayDir = subVelocity * timeIncrement (not unit * magnitude * dt) ✅ matches ActiveCast.luau fix
Cascading lag guard for both Always and Automatic modes
CancelHighResCast flag respected in PerformHighFidelitySubcasts
castTotalRuntime[i] advanced inside sub-cast loop so distance/position stay consistent
destroyedIds deduplication — same index could be inserted twice (hit + maxDistance in Always) ⚠️ Add a guard: if not table.find(destroyedIds, i) then table.insert(destroyedIds, i) end in both the hit and maxDistance checks inside PerformHighFidelitySubcasts

…A simulations

- Add HighFidelityBehavior.Automatic subcast verification in UpdateCasts
- When ray displacement exceeds segment size, perform subcast re-validation
- Update bullet position to accurate hit point after subcast verification
- Applied to both ParallelSimulation and SerialSimulation (SoA pattern)
- Add castCanPierceFn array for storing pierce callback functions
- Support piercing in both normal hit handling and HighFidelity subcasts
- Fire Pierced event when CanPierce returns true, continue simulation
- Fire Hit event when CanPierce returns false or is nil
- Applied to both ParallelSimulation and SerialSimulation
- Add castEventsConfig, castEventsModuleConfig, castEventsModule SoA arrays
- Update QueueFire to check both FastCastEventsConfig and FastCastEventsModuleConfig
- Fire module callbacks directly when FastCastEventsModule is set
- Fire CastFire, CastTerminating events with proper gating
CastFire should fire: (cast, origin, direction, velocity, behavior)
Not: (cast, origin, velocity, acceleration)
Comment out the SimulateCast and Stepped functions for future reference.
Add DebugLogging checks for:
- Casting (per frame)
- Hit detection
- RayPierce (piercing function returns, no function set)
- Calculation (subcast info)
- Segment (per-segment subcast iteration)

Matches debug output in legacy ActiveCast.luau
Add visualization functions for ray/block/sphere segments and hit markers:
- VisualizeRaySegment, VisualizeBlockSegment, VisualizeSphereSegment
- VisualizeHit with pierce/hit color differentiation
- Uses task.synchronize in ParallelSimulation for cross-thread safety
- Visualize cast segments per frame, hit markers on hit/pierce events
- Matches legacy Debug_Segment/Hit/RayPierce colors
@weenachuangkud weenachuangkud marked this pull request as draft May 8, 2026 11:25
@weenachuangkud
Copy link
Copy Markdown
Owner Author

@coderabbitai summary

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

✅ Actions performed

Summary regeneration triggered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant