Documentation
¶
Overview ¶
Package bot — boot_report.go
BootReport is the structured, persisted, Wails-emitted record of one boot attempt. It is built incrementally by the BootOrchestrator and surfaces three places: the in-memory status (returned to the React side via the bot_init_report Wails event), the JSON file under paths.ResolveConfig("logs/"), and the bundled boot profile (p95 / recommended timeout — see bootprofile.go).
The intent is "never again should the user see a bare 'failed to initialize bot' line and have to grep app.log to find out why." Every step records its name, duration, result, and a short human detail. The SuggestedAction field is what the orchestrator believes the user should try next ("wait longer", "check adb", etc.) — the React panel surfaces this as a one-liner with optional buttons.
The BootReport type holds a sync.Mutex so the orchestrator can append steps and recovery names from multiple goroutines safely. BootReport is therefore NOT safe to copy by value — copying a sync.Mutex is undefined behavior. The companion BootReportView type below is the value-copyable, JSON-marshalable, Wails-emittable projection: Snapshot() and SaveJSON() produce a BootReportView, and all read paths operate on it. This split keeps the mutex where it belongs (on the writer) and prevents the "go vet warning: return copies lock value" footgun.
Package bot — bootorchestrator.go ¶
BootOrchestrator is the single entry point that brings the bot from "user clicked Start" to "ready to capture frames". It replaces the inline boot sequence that used to live in NewBot() in bot.go, and turns the multi-step hand-rolled procedure into a state machine with named steps, layered recovery, structured reporting, and learned timing.
The orchestrator does NOT own the capture loop, the classifier, the attack executor, or any of the runtime subsystems. It only owns the BOOT phase. NewBot() now reads:
bctx, err := NewBootOrchestrator(cfg, client).Boot(ctx)
if err != nil { ... }
// bctx.ScreenW/H, bctx.Client, bctx.BlueStacksRestarted are ready
BootConfig carries the tunables. The defaults are production-safe; in dev mode NewBot shrinks them via WithDevFastFail so a failed boot surfaces in 25s instead of 180s+.
Threading: the orchestrator is single-use. Construct one per NewBot call, call Boot() once, and discard. The Client it receives is mutated (its transport is connected, the pipe is not enabled) — that's the intended hand-off.
Package bot — bootprofile.go ¶
BootProfile is the persistent, learned-timing store for boot durations. Each successful boot appends a sample; on timeout the recommended timeout is bumped (p95 * 1.5 capped at MaxRecommended). The orchestrator reads the recommended timeout before starting, so after one or two runs on a slow MacBook the bot stops timing out on a value that was hand-picked for fast hardware.
The file lives at paths.ResolveConfig("boot_profile.json") and is written atomically (tmp + rename) so a crash mid-write doesn't corrupt the existing profile.
Package bot — recovery.go ¶
RecoveryPolicy is the layered escalation engine that decides what to try when a boot step fails. The whole point of replacing the hard-coded "kill BlueStacks" with named strategies is that the bot tries the cheapest fixes first and only escalates to destructive actions as a last resort.
Strategy ladder (cheapest to most-destructive):
RetryTransport — close + reopen the ADB transport socket. Fast (~50ms). Handles the common case of a stale connection from BlueStacks restart.
SoftReset — `adb shell stop && adb shell start`. Restarts the Android runtime without killing BlueStacks. ~5-10s. Handles the "boot_completed never appeared" case where Android is up but wedged.
RelaunchGame — force-stop + restart CoC via monkey. Doesn't touch BlueStacks or the Android runtime. ~3-5s. Handles the "PackageManager up but CoC crashed" case.
RestartBlueStacks — osascript quit + open -a BlueStacks. ~10-15s for the launch. The last reasonable option before…
NuclearOption — kill -9 BlueStacks + rewrite config from scratch. ~20s. Only used when EnsureBlueStacksMac itself failed (config write rejected, etc).
Each strategy has metadata: the action function, a "what went wrong" predicate that decides when to try it, and a budget. The orchestrator walks the ladder, applying backoff between attempts, and surfaces a "SuggestedAction" string for the UI.
Index ¶
- Constants
- func AsyncWriteFile(path string, data []byte, perms os.FileMode) error
- func CPUTime() time.Duration
- func RunWallUpgradeLoop(h *WallUpgradeHooks)
- func SuggestedAction(lastStep, lastStrategy, lastErr string) string
- type AsyncWriter
- type AttackReport
- type BootConfig
- type BootContext
- type BootOrchestrator
- type BootProfile
- type BootProfileSample
- type BootReport
- func (r *BootReport) AppendStep(name string, startedAt time.Time, result BootResult, detail string)
- func (r *BootReport) Complete(outcome string, err error, suggestedAction string)
- func (r *BootReport) JoinedStepSummary(maxLen int) string
- func (r *BootReport) MarkRecovery(strategy string)
- func (r *BootReport) SaveJSON(path string) error
- func (r *BootReport) SetAttempts(n int)
- func (r *BootReport) SetBlueStacksEnsured(v bool)
- func (r *BootReport) SetDeviceContext(deviceID, packageName string, width, height int)
- func (r *BootReport) SetMetadata(kv map[string]string)
- func (r *BootReport) Snapshot() BootReportView
- func (r *BootReport) Summary() string
- type BootReportView
- type BootResult
- type BootStep
- type Bot
- func (b *Bot) Cancel()
- func (b *Bot) DumpDiagnostics(reason string, screen gocv.Mat, context map[string]interface{}) error
- func (b *Bot) GetClient() *adb.Client
- func (b *Bot) GetLastFrame() string
- func (b *Bot) Health() game.SystemHealth
- func (b *Bot) QuickDeploy() error
- func (b *Bot) Start() error
- func (b *Bot) Stats() BotStats
- func (b *Bot) Stop()
- func (b *Bot) UpdateConfig(cfg *config.BotConfig)
- func (b *Bot) UpgradeWalls(gc *game.GameContext)
- type BotStats
- type DiagnosticData
- type Pinpoint
- type RecoveryConfig
- type RecoveryPolicy
- type RecoveryStrategy
- type Rect
- type WallUpgradeHooks
Constants ¶
const DefaultRecommendedTimeoutMs = 90_000
DefaultRecommendedTimeoutMs is the initial recommended boot timeout when no profile exists yet. 90s matches the prior hard-coded value in bot.go so behavior is unchanged on first launch.
const MaxBootProfileSamples = 30
MaxBootProfileSamples is the rolling window of recent boots the profile keeps. 30 is enough to absorb a few bad days without forgetting that "this user usually boots in 12s."
const MaxRecommendedTimeoutMs = 300_000
MaxRecommendedTimeoutMs is the upper cap on the recommended timeout. Even on a very slow machine, 5 minutes is enough — past that, something is actually wrong and the user should investigate rather than have the bot silently retry for 10 minutes.
const MinRecommendedTimeoutMs = 30_000
MinRecommendedTimeoutMs is the lower bound. 30s is a reasonable floor: any device that boots faster than 30s on a clean run is already happy at the floor, and bumping below 30s just causes spurious timeouts on noisy hardware.
Variables ¶
This section is empty.
Functions ¶
func CPUTime ¶
CPUTime returns the total CPU time consumed by this process since it started, as an absolute duration. It is the sum of user + system time reported by the kernel (getrusage RUSAGE_SELF).
Unlike a percentage, this number is device-independent: it means the same thing on an M1, an M3 Max, or any other machine. A busy-loop burning one core for 10 seconds reports ~10s here regardless of how many cores the host has. To compare efficiency across devices, measure CPUTime() delta over a fixed wall-clock window (see CPUUsage).
func RunWallUpgradeLoop ¶
func RunWallUpgradeLoop(h *WallUpgradeHooks)
RunWallUpgradeLoop drives the wall-upgrade sequence with explicit deps (parametrised by h). Production wires a Bot-built hooks struct; the cmd/test_wall_upgrade diagnostic tool wires its own hooks struct against a live adb.Client without instantiating a full *Bot.
Flow selection (in priority order):
ASSET-DRIVEN (BLIND-TAP) — when wall_upgrade_buttons.json + wall_upgrade_confirm.json + wall_upgrade_x_roi.json ALL load cleanly: a. Tap gold rect Center. b. Tap Confirm rect Center (BLIND — no template match). c. Wait + capture. Then check BOTH hasModalInRect(x_popup_roi) AND hasModalInRect(x_popup_roi_alt) (alt only if configured). Chained-popup support per the user's spec: dismissing the gem-buy modal can reveal a SECOND popup whose X lives at x_popup_roi_alt; both must be down before declaring success. d. If ANY popup is still up, single-tap each X in sequence: primary center, then alt center (if configured). ONE tap per X, no retry, no offsets. Per the user's "click X, then click X again in the confirm menu" spec. e. Verify both rects dismissed after the sequence. If either is still up → fall through to the NEXT button on this same wall (gold → elixir per spec "if gold was unsuccessful, click elixir"), emitting primary_still_up / alt_still_up diagnostics so the user knows which rect mis-picked. If BOTH buttons fire this path, the post-button-loop check surfaces the `all_unaffordable` exit and the sequence ends. Wall-level aborts only fire on defensive capture failures (modalErr / verErr — transport sick), not on rect mis-picks. f. Both rects down → silent spawn = success. Move to next button.
PROBE-AND-DISCARD — when only wall_upgrade_buttons.json loads (legacy mode from the original pre-rect refactor): - Tap gold → if btn_confirm_upgrade template matches ∧ checkConfirmRed says white → tap template-derived confirm. - Tap gold → confirm missing → silent spawn = success. - Tap gold → confirm shown + checkConfirmRed says red → tap X. - Then loop to elixir with the same logic.
LEGACY TEMPLATE — when no rect assets load: - For each btn_upgrade_wall candidate (multi-scale match): pre-tap cost-color check via costROI, then probe-and-discard with btn_confirm_upgrade template match.
Each phase boundary emits an OnStep event so the diagnostic tool can capture + annotate a screenshot at every decision point. Payloads follow the Mat-ownership contract documented on WallUpgradeHooks.step.
func SuggestedAction ¶
SuggestedAction is the human-readable "what to try next" string surfaced in the BootReport and the React UI. It takes the most recent failed step + the last strategy attempted and produces a one-liner. Kept short (1 sentence) so it fits a UI card.
Types ¶
type AsyncWriter ¶
type AsyncWriter struct {
// contains filtered or unexported fields
}
func NewAsyncWriter ¶
func NewAsyncWriter() *AsyncWriter
func (*AsyncWriter) Close ¶
func (aw *AsyncWriter) Close()
type AttackReport ¶
type AttackReport struct {
Timestamp string `json:"timestamp"`
Strategy string `json:"strategy"`
TargetEdge string `json:"target_edge"`
DeploySuccess bool `json:"deploy_success"`
UndeployedSlots int `json:"undeployed_slots"`
DeployError string `json:"deploy_error,omitempty"`
ParsedResults bool `json:"parsed_results"`
Stars int `json:"stars"`
GoldStolen int `json:"gold_stolen"`
ElixirStolen int `json:"elixir_stolen"`
DarkElixirStolen int `json:"dark_elixir_stolen"`
BonusGold int `json:"bonus_gold"`
BonusElixir int `json:"bonus_elixir"`
BonusDE int `json:"bonus_de"`
TotalAttacks int32 `json:"total_attacks_session"`
}
type BootConfig ¶
type BootConfig struct {
// ADB timeouts.
AdbConnectTimeout time.Duration // overall budget for the transport connect loop
AdbConnectPoll time.Duration // gap between connect attempts
AdbPerCallTimeout time.Duration // passed to transport.Exec (one Shell call)
// Boot-probe timeouts (multi-signal).
BootProbeTimeout time.Duration // overall budget for the probe loop
BootProbePoll time.Duration // gap between probe passes
BootProbeMinSignals int // required # of ready signals (2 = 2-of-4)
BootProbePerSignal time.Duration // per-signal timeout inside one probe pass
// Recovery knobs.
MaxRecoveryAttempts int
AllowNuclear bool
InitialRecoveryBackoff time.Duration
MaxRecoveryBackoff time.Duration
// Misc.
WaitForGameSettle time.Duration // post-StartApp sleep
PackageName string
DeviceID string
ExpectedWidth int
ExpectedHeight int
ExpectedDPI int
// DevFastFail is true when wails dev is running. Shrinks all
// the timeouts above; the orchestrator does NOT touch this
// field directly (NewBootConfigFromBotConfig reads the env).
DevFastFail bool
}
BootConfig is the per-call configuration for a single boot attempt. Constructed by NewBootConfigFromBotConfig; tune via the WithDevFastFail and WithAllowNuclear helpers.
func DefaultBootConfig ¶
func DefaultBootConfig() BootConfig
DefaultBootConfig returns the production defaults. 90s on the boot probe, 90s on the ADB connect loop, 2-of-4 signals, 5 recovery attempts with a 0.5s..4s exponential backoff.
func NewBootConfigFromBotConfig ¶
func NewBootConfigFromBotConfig(cfg *config.BotConfig) BootConfig
NewBootConfigFromBotConfig merges the BotConfig defaults with the orchestrator's own defaults. The DeviceID, PackageName, and resolution come from cfg.Device. The recovery policy reads cfg flags for future tunability (currently none — the orchestrator has its own opinion).
func (BootConfig) WithDevFastFail ¶
func (c BootConfig) WithDevFastFail() BootConfig
WithDevFastFail returns a copy of cfg with shrunken timeouts suitable for `wails dev`. 10s for the ADB loop, 15s for the boot probe, 500ms poll cadence, no nuclear option. A coding error in dev cycles through the failure modes in ~25s instead of 180s+.
type BootContext ¶
type BootContext struct {
Client *adb.Client
DeviceID string
PackageName string
ScreenW int
ScreenH int
ExpectedScreenW int
ExpectedScreenH int
BlueStacksRestarted bool
Report BootReportView
BootDuration time.Duration
RecoveryUsed []string
}
BootContext is what the orchestrator returns on success. NewBot reads ScreenW/H to build the Calibration; everything else is passed through to the Bot struct for runtime use.
Report is a value-copyable BootReportView (no mutex), safe to pass across goroutines, serialize, or emit via Wails.
type BootOrchestrator ¶
type BootOrchestrator struct {
// contains filtered or unexported fields
}
BootOrchestrator is the live state of a single boot attempt. One per NewBot call. Use NewBootOrchestrator to construct.
func NewBootOrchestrator ¶
func NewBootOrchestrator(cfg BootConfig, client *adb.Client, logger zerolog.Logger) *BootOrchestrator
NewBootOrchestrator wires the orchestrator. The client is the already-constructed adb.Client from NewBot (no transport connect has happened yet — the orchestrator does that itself).
func (*BootOrchestrator) Boot ¶
func (o *BootOrchestrator) Boot(ctx context.Context) (*BootContext, error)
Boot runs the full sequence. The returned BootContext is non-nil iff err is nil. On failure the BootContext is nil; use orchestrator.Report().Snapshot() to get the structured report.
func (*BootOrchestrator) Report ¶
func (o *BootOrchestrator) Report() *BootReport
Report returns the live (not snapshot) report. Callers that need to serialize should use Report().Snapshot() instead.
type BootProfile ¶
type BootProfile struct {
DeviceID string `json:"device_id,omitempty"`
Samples []BootProfileSample `json:"samples"`
LastUpdated time.Time `json:"last_updated"`
RecommendedMs int `json:"recommended_timeout_ms"`
// contains filtered or unexported fields
}
BootProfile is the persistent learned-timing record for a single device. Currently the file is per-host (one profile, not per device), but the DeviceID field is kept on every sample so a future per-device split is a one-line change.
func LoadBootProfile ¶
func LoadBootProfile(path string) (*BootProfile, error)
LoadBootProfile reads path from disk. A missing file is not an error — the caller gets a default profile. Any other read or parse failure is returned wrapped.
func NewBootProfile ¶
func NewBootProfile() *BootProfile
NewBootProfile returns a fresh, empty profile. Use LoadBootProfile to read an existing one from disk.
func (*BootProfile) AddSample ¶
func (p *BootProfile) AddSample(s BootProfileSample)
AddSample appends a sample and recomputes the recommended timeout in one atomic-with-respect-to-readers step. Trims the rolling window to MaxBootProfileSamples so the file stays small.
Recommendation rule:
- If any sample is a "timeout", the recommended timeout is bumped to max(p95(all) * 1.5, currentRecommended).
- Otherwise the recommended timeout is max(p95(successful) * 1.5, MinRecommended).
- Always clamped to [Min, Max].
1.5x headroom covers a single noisy run without overcorrecting.
func (*BootProfile) RecommendedTimeout ¶
func (p *BootProfile) RecommendedTimeout() time.Duration
RecommendedTimeout returns the recommended boot timeout in milliseconds, clamped to [Min, Max]. The mutex is held only for the read; recomputation is cheap.
func (*BootProfile) Save ¶
func (p *BootProfile) Save(path string) error
Save writes the profile to path atomically (write to .tmp, fsync, rename). The mutex is held only over the in-memory mutation; the disk write is best-effort and any error is returned to the caller.
type BootProfileSample ¶
type BootProfileSample struct {
StartedAt time.Time `json:"started_at"`
Duration int64 `json:"duration_ms"`
Outcome string `json:"outcome"`
}
BootProfileSample is one observation. Outcome is "ok" or "timeout" — both are recorded so the recommended timeout adapts to BOTH typical-case speed AND the worst-case stretch the user has hit.
type BootReport ¶
type BootReport struct {
// contains filtered or unexported fields
}
BootReport is the full per-attempt record. It is safe for concurrent writes via the embedded mutex; the orchestrator holds the lock while appending steps. DO NOT copy a BootReport by value — use Snapshot() to get a value-copyable BootReportView for reads, JSON marshaling, or Wails emission.
The mutex is embedded (not a pointer) so the zero value is usable without explicit initialization. NewBootReport() handles the slice and map allocations.
func NewBootReport ¶
func NewBootReport() *BootReport
NewBootReport constructs an empty report with the current wall clock as the start time. The caller is expected to fill DeviceID, PackageName, ExpectedWidth/Height, and Metadata before appending the first step (or, at minimum, before Snapshot() / Complete()).
func (*BootReport) AppendStep ¶
func (r *BootReport) AppendStep(name string, startedAt time.Time, result BootResult, detail string)
AppendStep records a completed step. Safe for concurrent use. The passed-in startedAt is when the step began; Duration is recomputed here to keep callers from getting the wall-clock math wrong.
func (*BootReport) Complete ¶
func (r *BootReport) Complete(outcome string, err error, suggestedAction string)
Complete finalizes the report. outcome should be "ok" or "failed". err may be nil for "ok". suggestedAction is a short imperative like "wait longer" or "relaunch BlueStacks manually" — surfaced in the UI and the JSON file.
func (*BootReport) JoinedStepSummary ¶
func (r *BootReport) JoinedStepSummary(maxLen int) string
JoinedStepSummary returns "adb.connect=ok/2.1s | boot.probe=ok/87s | ..." suitable for one-line log emission and the Wails bot_init_report event payload. Truncates to maxLen characters with a trailing "…" so a 90-second probe doesn't blow up a UI card.
func (*BootReport) MarkRecovery ¶
func (r *BootReport) MarkRecovery(strategy string)
MarkRecovery appends a recovery-strategy name to the report. Called once per executed strategy (not per attempt) so the user can see "we had to SoftReset this time" in the UI.
func (*BootReport) SaveJSON ¶
func (r *BootReport) SaveJSON(path string) error
SaveJSON writes the report to path as pretty-printed JSON. Errors are returned to the caller but should typically be logged at debug level — a failed persistence must not itself become a boot failure.
func (*BootReport) SetAttempts ¶
func (r *BootReport) SetAttempts(n int)
SetAttempts records the number of probe attempts the orchestrator made. Called once at the end of the boot.
func (*BootReport) SetBlueStacksEnsured ¶
func (r *BootReport) SetBlueStacksEnsured(v bool)
SetBlueStacksEnsured marks whether EnsureBlueStacksMac was called during this boot. Used by the UI to distinguish "cold-started BlueStacks" from "reused an already-running instance."
func (*BootReport) SetDeviceContext ¶
func (r *BootReport) SetDeviceContext(deviceID, packageName string, width, height int)
SetDeviceContext sets the device/package/resolution fields. Called once at construction so Snapshot() can include them in the view. Safe to call before any concurrent writer exists.
func (*BootReport) SetMetadata ¶
func (r *BootReport) SetMetadata(kv map[string]string)
SetMetadata merges key/value pairs into the report's free-form metadata. Safe for concurrent use. Existing keys are overwritten; new keys are added.
func (*BootReport) Snapshot ¶
func (r *BootReport) Snapshot() BootReportView
Snapshot returns a value-copyable, JSON-marshalable view of the report. The mutex is held only long enough to clone the slices and map; the returned BootReportView is independent of the source BootReport and can be passed across goroutines, marshaled, or emitted via Wails without any lock-copy issues.
func (*BootReport) Summary ¶
func (r *BootReport) Summary() string
Summary returns a one-line human description for log lines. Format:
"boot ok in 12.3s (4 steps, no recovery)" — success "boot failed in 90.0s: android boot timeout (5 steps, 2 recovery)" — failure
type BootReportView ¶
type BootReportView struct {
StartedAt time.Time `json:"started_at"`
CompletedAt time.Time `json:"completed_at,omitempty"`
Duration time.Duration `json:"duration_ns"`
Outcome string `json:"outcome"` // "ok" | "failed"
FinalError string `json:"final_error,omitempty"`
SuggestedAction string `json:"suggested_action,omitempty"`
DeviceID string `json:"device_id,omitempty"`
PackageName string `json:"package_name,omitempty"`
ExpectedWidth int `json:"expected_width,omitempty"`
ExpectedHeight int `json:"expected_height,omitempty"`
BlueStacksEnsured bool `json:"bluestacks_ensured"`
RecoveryUsed []string `json:"recovery_used,omitempty"`
Attempts int `json:"attempts"`
Steps []BootStep `json:"steps"`
Metadata map[string]string `json:"metadata,omitempty"`
}
BootReportView is the JSON-serializable, Wails-emittable, copyable view of a BootReport. It has the same fields as BootReport minus the mutex, and is what callers actually read/serialize. The orchestrator hands out views via Snapshot() and the React side consumes them as plain JSON.
func LoadBootReportJSON ¶
func LoadBootReportJSON(path string) (BootReportView, error)
LoadBootReportJSON parses a previously-saved BootReport from disk into a BootReportView. The view is what callers actually want — they don't need a mutex-protected writer for reading. Returns os.ErrNotExist on a missing file (not an error condition).
type BootResult ¶
type BootResult string
BootResult is the terminal outcome of a single step in the boot pipeline. We keep it as a small enum-style string so it round-trips through JSON cleanly and is grep-friendly in app.log.
const ( BootResultOK BootResult = "ok" BootResultTimeout BootResult = "timeout" BootResultError BootResult = "error" BootResultSkipped BootResult = "skipped" )
type BootStep ¶
type BootStep struct {
Name string `json:"name"` // e.g. "adb.connect", "bluestacks.ensure", "boot.probe"
StartedAt time.Time `json:"started_at"` // RFC3339Nano
Duration time.Duration `json:"duration_ns"` // wall-clock duration of this step
Result BootResult `json:"result"` // ok | timeout | error | skipped
Detail string `json:"detail,omitempty"` // human-readable context (error string, signal name, etc.)
}
BootStep is one discrete phase of the boot sequence. The orchestrator emits one per phase. Latency at the field level is the per-step duration; cumulative timing is recomputed by Summary().
func (BootStep) StepSummary ¶
StepSummary is a small printable summary for the React side / log line. One per step, joined with " | " by JoinedStepSummary.
type Bot ¶
type Bot struct {
OnFrame func(string)
OnStatsUpdate func()
// contains filtered or unexported fields
}
func (*Bot) Cancel ¶
func (b *Bot) Cancel()
Cancel signals the bot's context to stop. The captureLoop and any in-flight attack sequence see the cancellation on their next `b.ctx.Done()` check (sub-millisecond), which is what makes the Stop button feel "instant" to the user — no more taps, captures, state transitions, or attack progression. The full `Stop()` path (ADB close, async-writer drain, file flush) is intentionally kept out of this method so callers can split the work: `Cancel()` for the synchronous "stop what you're doing" signal, and `Stop()` for the heavier teardown that App.StopBot detaches into a goroutine.
func (*Bot) DumpDiagnostics ¶
DumpDiagnostics saves a screenshot and a JSON file containing the bot's state.
func (*Bot) GetLastFrame ¶
func (*Bot) Health ¶
func (b *Bot) Health() game.SystemHealth
func (*Bot) QuickDeploy ¶
QuickDeploy is the manual single-shot deploy path for run_designed_attack.sh. The user is assumed to already be on the attack screen with a base loaded — there's no search loop, no attack-button discovery, no home → finds-match pipeline. We capture the current screen once, run deployTroops (which honors formula.json overrides if design_attack wrote one next to the strategy), and return.
The captureLoop is intentionally NOT started; cli.go's --deploy-only branch calls this directly and bypasses b.Start() to avoid the Find-Attack-Button race that would otherwise kick the bot into the next-base search cycle.
On non-Battle screens (e.g. user is still on home, or in clouds), we log a WARN and proceed anyway — the deployTroops path will see the absence of a troop bar and either error out cleanly or succeed-by-luck if the screen does actually contain a deployable base. Caller should surface the error.
func (*Bot) UpdateConfig ¶
func (*Bot) UpgradeWalls ¶
func (b *Bot) UpgradeWalls(gc *game.GameContext)
UpgradeWalls executes the wall-upgrade sequence repeatedly until no more affordable options exist. After this refactor it is a thin wrapper that delegates to runWallUpgradeLoop with the production Bot's dependencies. The diagnostic tool at cmd/test_wall_upgrade calls runWallUpgradeLoop directly with a hand-built hooks struct.
type BotStats ¶
type BotStats struct {
AttacksCompleted int32 `json:"attacks_completed"`
SearchSkips int32 `json:"search_skips"`
TotalGold int64 `json:"total_gold"`
TotalElixir int64 `json:"total_elixir"`
TotalDE int64 `json:"total_de"`
Stars0 int32 `json:"stars_0"`
Stars1 int32 `json:"stars_1"`
Stars2 int32 `json:"stars_2"`
Stars3 int32 `json:"stars_3"`
Uptime time.Duration `json:"uptime"`
AdbHealth adb.Health `json:"adb_health"`
// CPUTimeSec is the absolute CPU time consumed since process start
// (device-independent, unlike "% CPU").
CPUTimeSec float64 `json:"cpu_time_sec"`
// CPUCores is CPU usage as a fraction of one core over the last sample
// window (1.0 == one full core busy).
CPUCores float64 `json:"cpu_cores"`
}
type DiagnosticData ¶
type DiagnosticData struct {
Timestamp time.Time `json:"timestamp"`
Reason string `json:"reason"`
State string `json:"state"`
Context map[string]interface{} `json:"context,omitempty"`
}
DiagnosticData holds the state of the bot at the time of failure.
type Pinpoint ¶
Pinpoint defines a precise location on the reference screen (860x732) and a color check to verify it before clicking.
type RecoveryConfig ¶
type RecoveryConfig struct {
// MaxAttempts is the cap on the whole boot sequence (NOT per
// strategy). Default 5: one initial try + four escalations.
MaxAttempts int
// AllowNuclear gates the RestartBlueStacks and NuclearOption
// strategies. In dev mode the orchestrator sets this to false so
// a coding error can't trigger a BlueStacks restart loop in
// the dev's editor.
AllowNuclear bool
// InitialBackoff is the wait before the SECOND attempt. Each
// subsequent attempt doubles, capped at MaxBackoff. 500ms is a
// reasonable default — long enough to let a shell call return
// its real result, short enough that a human doesn't notice.
InitialBackoff time.Duration
MaxBackoff time.Duration
}
RecoveryConfig is the orchestration knobs the policy consults.
func DefaultRecoveryConfig ¶
func DefaultRecoveryConfig() RecoveryConfig
DefaultRecoveryConfig returns the production defaults.
type RecoveryPolicy ¶
type RecoveryPolicy struct {
// contains filtered or unexported fields
}
RecoveryPolicy wires the strategy ladder to the live Client. It is constructed once by the orchestrator and consulted at most once per attempt. Stateless after construction.
func NewRecoveryPolicy ¶
func NewRecoveryPolicy(cfg RecoveryConfig, client adbClient) *RecoveryPolicy
NewRecoveryPolicy constructs a policy. The client is the live ADB client (or a fake in tests); the cfg carries the budgets.
func (*RecoveryPolicy) Backoff ¶
func (p *RecoveryPolicy) Backoff(attempt int) time.Duration
Backoff returns the wait time before the next attempt. Exponential with a cap, starting from cfg.InitialBackoff. attempt is 1-based.
func (*RecoveryPolicy) Escalate ¶
func (p *RecoveryPolicy) Escalate(strats []RecoveryStrategy, attempt int) int
Escalate decides which strategy (by index in Strategies()) to try next. failureContext tells the policy what step failed; the return value is the index into Strategies() to try, or -1 if the ladder is exhausted.
The current implementation is a simple linear walk: it always tries the next strategy on each escalation. A future enhancement could skip strategies whose "predicate" doesn't match the failure (e.g. don't try RelaunchGame if the failure is in adb.connect itself).
func (*RecoveryPolicy) Strategies ¶
func (p *RecoveryPolicy) Strategies(packageName string, w, h, dpi int) []RecoveryStrategy
Strategies returns the ordered ladder for the current config. The returned slice is a fresh copy — callers can mutate without affecting future ladders.
Order rationale (cheapest to most-destructive, with the diagnostics- first principle applied):
- RetryTransport — close + reopen the ADB transport socket. Fast (~50ms). Handles a stale connection from a fresh BlueStacks start.
- ResetAdbServer — `adb kill-server` + `adb start-server`. ~3s. Handles the "localhost:5555 listed as offline in `adb devices` even though the device is up" failure mode that hits after a hard kill or a wails-dev session. Non-destructive of the emulator state but does drop ALL adb connections globally — noted in the BootReport.
- SoftReset — `adb shell stop && adb shell start`. Restarts the Android runtime without killing BlueStacks. ~8s. Handles Android wedged at the runtime layer.
- RelaunchGame — force-stop + restart CoC via monkey. Doesn't touch BlueStacks or Android. ~5s. Useless if ADB isn't yet talking to a device, so in practice only invoked from the boot-probe ladder.
- RestartBlueStacks — osascript quit + open -a BlueStacks. ~15s. The nuclear option, only used when AllowNuclear is true and everything above has failed.
type RecoveryStrategy ¶
type RecoveryStrategy struct {
Name string
// Apply is the side-effecting action. It returns nil on success
// or an error if the strategy itself failed (e.g. SoftReset
// timeout). The error is reported on the BootReport.
Apply func(ctx context.Context) error
// Cost is a rough estimate of how long Apply takes. Used only
// for the "total recovery time" reporting — not for budgeting.
Cost time.Duration
// Destructive is true for strategies that kill the emulator or
// the game. The orchestrator's AllowNuclear flag controls whether
// strategies with Destructive=true are ever invoked.
Destructive bool
}
RecoveryStrategy is one named escalation step.
type Rect ¶
Rect is the JSON-friendly shape for picker'd tap regions. Each of the three wall-upgrade asset files
- assets/wall_upgrade_buttons.json (top-level: gold, elixir)
- assets/wall_upgrade_confirm.json (top-level: confirm_button)
- assets/wall_upgrade_x_roi.json (top-level: x_popup_roi)
writes a top-level dict with one or more {x1, y1, x2, y2} rects, which this struct decodes uniformly.
Coords are PHYSICAL pixels captured at the picker session's actual screen size. Per the user's calibration baseline (860x732 on BlueStacks Air + adb screencap at the device's native frame), the picker and the bot's Cal.ScaleX/ScaleY agree at 1:1, so JSON values land directly in bot tap coords without re-scaling. Mismatch on a different device frame is a separate concern — fix at picker or bot, not within the loader.
The legacy tap pattern in earlier versions of this file used the image.Rectangle stdlib type via image.Rect(x1,y1,x2,y2). image.Rectangle serializes to JSON as `{"Min":{"X":x,"Y":y},"Max":{"X":x2,"Y":y2}}`, which doesn't match the picker's flat x1/y1/x2/y2 output. The Rect struct here decodes the flat shape 1:1 with no per-file ad-hoc map[string]int shape, so adding a new asset file is one struct-tag each.
func (Rect) Center ¶
Center returns ((X1+X2)/2, (Y1+Y2)/2). Integer truncation matches adb input.tap's nearest-pixel rounding, so a 153-wide rect lands at ±0.5 px off geometric center — visually indistinguishable from the user's drag.
func (Rect) Empty ¶
Empty returns true if the rect has zero area — i.e., zero width (X1==X2) OR zero height (Y1==Y2). The picker writes degenerate (0,0,0,0) only when handed a broken drag; the loaders use Empty() to reject that so the bot's tap path starts with `ok=false` rather than emitting a 0-area tap region that would silently tap the top-left corner of the screen. The all-zero (0,0,0,0) case is covered by the X1==X2 path because (X1==X2 AND Y1==Y2) is a subset of (X1==X2 OR Y1==Y2).
type WallUpgradeHooks ¶
type WallUpgradeHooks struct {
Logger zerolog.Logger
Client wallClient
Cal *game.Calibration
Templates *game.TemplateStore
// Classify is invoked with each capture during the MainVillage
// verify loop and to detect interruption dialogs. May be nil.
Classify func(gocv.Mat) (game.GameState, int)
// Dismiss taps a neutral background area to clear any active
// wall-selection after a failed button-template match. May be nil.
Dismiss func()
// OnStep is the optional phase-boundary instrumentation hook.
OnStep func(step string, data map[string]any)
}
WallUpgradeHooks groups the dependencies a wall-upgrade loop iteration needs. Used both by Bot.UpgradeWalls (production path) and the cmd/test_wall_upgrade diagnostic tool.
Optional fields:
- Classify: nil disables state verification + interruption dismissal (the diagnostic tool's "manual" mode assumes the user navigated the game into MainVillage themselves and skips dialog handling).
- OnStep: nil silences all instrumentation events. When wired, each phase boundary calls OnStep("phase_name", data). The diagnostic tool uses this to save annotated screenshots per phase.
- Dismiss: nil makes the loop's fallback skip the neutral-tap dismiss (e.g. when btn_upgrade_wall or btn_confirm_upgrade fail matching). Production wires this to Bot.dismissSelection.