Public/New-BluestacksUserScript.ps1

function New-BluestacksUserScript {
  <#
  .SYNOPSIS
    Compiles a sequence of gesture steps into a BlueStacks event array.

  .DESCRIPTION
    Iterates over a sequence of step hashtables and builds a flat array of
    BlueStacks input events. Each step advances an internal timestamp cursor
    so events are laid out contiguously in time.

    Supported step types:

      Snippet - embed a pre-recorded snippet from the snippet directory
      Click - inline tap: @(X, Y)
      Swipe - drag gesture with Start/End coordinates
      CircleClick - evenly-spaced taps around a circle
      Wait - silent pause; no events emitted

    Common keys accepted by all action steps:

      Repeat - repeat the action N times (default 1)
      RepeatInterval - ms gap between repetitions (default 20)
      HoldMs - tap-hold duration for Click steps (default 20)
      DelayAfter - ms of silence after the step (default 20)

  .PARAMETER Sequence
    Array of step hashtables defining the macro.

  .PARAMETER SnippetDir
    Directory that contains snippet JSON files.
    Defaults to the module-owned 'snippets' sub-folder.

  .PARAMETER StartOffset
    Milliseconds before the very first event. Defaults to 100.

  .PARAMETER Preview
    When specified, writes a human-readable timing summary to the host
    without modifying any files.

  .OUTPUTS
    PSCustomObject with properties: Events, EventCount, StartOffset,
                    FinalCursor, DurationMs, DurationLabel, SnippetDirectory.

  .EXAMPLE
    $result = New-BluestacksUserScript -Sequence @(
        @{ Click = @(26, 40) }
        @{ Wait = 2000 }
        @{ Click = @(50, 50); Repeat = 3; RepeatInterval = 500 }
    )
    $result.DurationLabel

  .EXAMPLE
    New-BluestacksUserScript -Sequence $sequence -Preview
  #>

  [CmdletBinding()]
  [OutputType([PSCustomObject])]
  param(
    [Parameter(Mandatory)]
    [object[]] $Sequence,

    [string] $SnippetDir = (Get-BluestacksSnippetDirectory),
    [int]    $StartOffset = 100,
    [switch] $Preview
  )

  $resolvedSnippetDir = Get-BluestacksSnippetDirectory -SnippetDir $SnippetDir
  $events = [System.Collections.Generic.List[PSCustomObject]]::new()
  [long] $cursor = $StartOffset

  foreach ($step in $Sequence) {
    $repeat = $step.ContainsKey('Repeat') ? [int]$step.Repeat : 1
    $repeatInterval = $step.ContainsKey('RepeatInterval') ? [int]$step.RepeatInterval : 20
    $delayAfter = $step.ContainsKey('DelayAfter') ? [int]$step.DelayAfter : 20
    $holdMs = $step.ContainsKey('HoldMs') ? [int]$step.HoldMs : 20

    # -- Wait ------------------------------------------------------------------------
    if ($step.ContainsKey('Wait')) {
      $waitMs = [long]$step.Wait
      if ($Preview) {
        Write-Host (' [cursor {0,9} ms] WAIT {1} ms -> cursor {2} ms' -f $cursor, $waitMs, ($cursor + $waitMs))
      }
      $cursor += $waitMs
      continue
    }

    # -- Snippet ---------------------------------------------------------------------
    if ($step.ContainsKey('Snippet')) {
      $name = [string]$step.Snippet
      $duration = Get-BluestacksSnippetDuration -Name $name -SnippetDir $resolvedSnippetDir

      if ($Preview) {
        Write-Host (" [cursor {0,9} ms] SNIPPET '{1}' x{2} (dur {3} ms, +{4} ms interval, +{5} ms after)" -f
          $cursor, $name, $repeat, $duration, $repeatInterval, $delayAfter)
      }

      for ($i = 0; $i -lt $repeat; $i++) {
        $events.AddRange([PSCustomObject[]](Copy-BluestacksSnippetAt -Name $name -BaseTimestamp $cursor -SnippetDir $resolvedSnippetDir))
        $cursor += $duration
        if ($i -lt ($repeat - 1)) {
          $cursor += $repeatInterval
        }
      }

      $cursor += $delayAfter
      continue
    }

    # -- Click -----------------------------------------------------------------------
    if ($step.ContainsKey('Click')) {
      $x = [double]$step.Click[0]
      $y = [double]$step.Click[1]

      if ($Preview) {
        Write-Host (' [cursor {0,9} ms] CLICK ({1}, {2}) x{3} (hold {4} ms, +{5} ms interval, +{6} ms after)' -f
          $cursor, $x, $y, $repeat, $holdMs, $repeatInterval, $delayAfter)
      }

      for ($i = 0; $i -lt $repeat; $i++) {
        $events.AddRange([PSCustomObject[]](New-BluestacksClickEvents -X $x -Y $y -BaseTimestamp $cursor -HoldMs $holdMs))
        $cursor += $holdMs
        if ($i -lt ($repeat - 1)) {
          $cursor += $repeatInterval
        }
      }

      $cursor += $delayAfter
      continue
    }

    # -- Swipe -----------------------------------------------------------------------
    if ($step.ContainsKey('Swipe')) {
      $swipe = $step.Swipe

      $startX = $swipe.ContainsKey('Start') ? [double]$swipe.Start[0] : [double]$swipe.StartX
      $startY = $swipe.ContainsKey('Start') ? [double]$swipe.Start[1] : [double]$swipe.StartY

      $endX = $swipe.ContainsKey('End') ? [double]$swipe.End[0] : [double]$swipe.EndX
      $endY = $swipe.ContainsKey('End') ? [double]$swipe.End[1] : [double]$swipe.EndY

      $moveCount = $swipe.ContainsKey('MoveCount') ? [int]$swipe.MoveCount : 23
      $moveIntervalMs = $swipe.ContainsKey('MoveIntervalMs') ? [int]$swipe.MoveIntervalMs : 5
      $duration = ($moveCount + 1) * $moveIntervalMs

      if ($Preview) {
        Write-Host (' [cursor {0,9} ms] SWIPE ({1},{2})->({3},{4}) x{5} (moves {6}, every {7} ms, +{8} ms interval, +{9} ms after)' -f
          $cursor, $startX, $startY, $endX, $endY, $repeat, $moveCount, $moveIntervalMs, $repeatInterval, $delayAfter)
      }

      for ($i = 0; $i -lt $repeat; $i++) {
        $events.AddRange([PSCustomObject[]](New-BluestacksSwipeEvents -StartX $startX -StartY $startY -EndX $endX -EndY $endY `
              -BaseTimestamp $cursor -MoveCount $moveCount -MoveIntervalMs $moveIntervalMs))
        $cursor += $duration
        if ($i -lt ($repeat - 1)) {
          $cursor += $repeatInterval
        }
      }

      $cursor += $delayAfter
      continue
    }

    # -- CircleClick -------------------------------------------------------------------
    if ($step.ContainsKey('CircleClick')) {
      $circle = $step.CircleClick
      $centerX = [double]$circle.CenterX
      $centerY = [double]$circle.CenterY
      $radius = [double]$circle.Radius
      $clickCount = [int]$circle.ClickCount
      $intervalMs = $circle.ContainsKey('IntervalMs') ? [int]$circle.IntervalMs : $repeatInterval
      $duration = ($holdMs * $clickCount) + ($intervalMs * ($clickCount - 1))

      if ($Preview) {
        Write-Host (' [cursor {0,9} ms] CIRCLECLICK center=({1},{2}) radius={3} clicks={4} x{5} (hold {6} ms, +{7} ms point interval, +{8} ms repeat interval, +{9} ms after)' -f
          $cursor, $centerX, $centerY, $radius, $clickCount, $repeat, $holdMs, $intervalMs, $repeatInterval, $delayAfter)
      }

      for ($i = 0; $i -lt $repeat; $i++) {
        $events.AddRange([PSCustomObject[]](New-BluestacksCircleClickEvents -CenterX $centerX -CenterY $centerY -Radius $radius `
              -ClickCount $clickCount -BaseTimestamp $cursor -HoldMs $holdMs -IntervalMs $intervalMs))
        $cursor += $duration
        if ($i -lt ($repeat - 1)) {
          $cursor += $repeatInterval
        }
      }

      $cursor += $delayAfter
      continue
    }

    Write-Warning "Step has no recognised action key (Snippet / Click / Swipe / CircleClick / Wait). Keys found: $($step.Keys -join ', ')"
  }

  $durationMs = $cursor - $StartOffset
  $minutes = [math]::Floor($durationMs / 60000)
  $seconds = [math]::Round(($durationMs % 60000) / 1000, 1)

  return [PSCustomObject]@{
    Events = $events.ToArray()
    EventCount = $events.Count
    StartOffset = $StartOffset
    FinalCursor = $cursor
    DurationMs = $durationMs
    DurationLabel = '{0}m {1}s' -f $minutes, $seconds
    SnippetDirectory = $resolvedSnippetDir
  }
}