Skip to content

M2 Pro 4.1.1

Latest

Choose a tag to compare

@jriko9 jriko9 released this 08 Oct 21:14
· 1 commit to main since this release
e87f8d7

OBM2 Pro — CHANGELOG v4.1.1

Last updated: 2026-10-08

Fix — OCR block no longer fails to run

The OCR block failed at runtime and could not process an image. The underlying problems — engine
initialisation, loading of the native image libraries, the block's engine-mode default, and a couple
of edge cases in the block setup — have been fixed, and OCR now works end to end. The block also
surfaces the real underlying error now, instead of a generic wrapper message.

Fix — missing info strings for JWT / XOR / AWS4Signature function blocks

Selecting the JWT, XOR or AWS4Signature function in a Function block threw a handled
ResourceReferenceKeyNotFoundException (e.g. 'JWT' resource not found): the block loads the
function's description via FindResource(FunctionType.ToString()), but these three newer enum
members had no matching resource key in the string dictionaries, unlike every other function.
Added the three keys (JWT, XOR, AWS4Signature) with a localized description to all language
files (en-US / es-ES / it-IT). No more exception; the description panel now shows proper text.

UI — download counter badge no longer clipped

The downloads badge in the top-right title bar had a fixed Width="30", so the
count (formatted N0) was clipped once it reached 4 digits (e.g. 1,043). The
CloudDownload icon also had a negative left margin (-17), overlapping the number and
shifting it left. The badge now auto-sizes to its text and the icon sits before the number
with normal spacing, so the full count is always visible.

Request block — "Raw Body (keep \n)" option for JSON/GraphQL bodies

Added a Raw Body checkbox to the HTTP Request block (next to Url Encode Content). When enabled, the POST
body is sent verbatim: the engine no longer converts literal \n into real newlines and skips the .Unescape()
pass. This fixes GraphQL/JSON payloads copied from the browser (e.g. a registerDevice mutation whose query
string contains escaped \n), which previously failed with malformed.input because the request path
(RuriRequest.SetStandardContent) turned the escaped newlines into actual line breaks inside the JSON string,
breaking the body. Default off, so existing configs that rely on \n→newline are unchanged. New LoliScript
boolean RawBody; new i18n key Http_RawBody (en/it/es). The TLS Request block already sent the body verbatim,
so it was unaffected.

Hit DB — export the whole filtered collection to txt

Added an "Export Filtered" button (top-right of the Hit DB toolbar) that saves the DATA of the entire
filtered collection — every page, not just the one on screen — to a txt file (one entry per line, via a
Save dialog). Previously the only way to export more than one page was Select-All + Copy repeated on each
page. New VM method GetAllFilteredData() enumerates HitsCollection.Where(HitsFilter); new i18n keys
ExportFiltered / ExportFiltered_Tooltip (en/it/es).

Stacker — fix duplicated variables in the input dropdowns

After built-in variables were added to the suggestion dropdowns, Keycheck and Parse listed the built-ins
twice (their own local default list plus the shared one). The built-ins are now provided only by the
single shared source (StackVariableExtractor.GetCurrentContextVariables, deduplicated), and the local
default lists in Keycheck and Parse were removed. The templated forms <HEADERS{*}> / <COOKIES{*}>
moved into the shared built-in list so every block (Parse/Function/Keycheck) offers them once.

UI — editable ComboBox caret visible at the start of the text

In editable ComboBoxes (e.g. the Function block's Input String), the blinking caret was invisible when
the field was empty or when the cursor sat before the first character — it only appeared once typing moved
it right. The inner PART_EditableTextBox had Padding="0", so the caret at index 0 rendered on the
text box's left edge and was clipped. Added a 2px left padding to the editable text box in the shared
ComboBox template (App.xaml), giving the caret room; the text position shift is imperceptible. Applies to
all editable ComboBoxes.

Stacker — built-in variables in the input suggestions (Parse/Function/Keycheck)

The context-aware variable suggestion combo-boxes (Parse, Function, Keycheck) listed only the variables
declared by preceding blocks. They now also list the built-in global variables (<SOURCE>,
<RESPONSECODE>, <ADDRESS>, <HEADERS(*)>, <COOKIES(*)>, <ERROR>, <STATUS>, <PROXY>,
<PROXYFULL>, <INPUT>, <BOTNUM>, <RETRIES>, <OCRRATE>, <BASEDIR>), shown after the declared
ones — like OB2's variable dictionary. These are always offered, including on the first block (the
list was previously empty there).

LoliScript — new <PROXYFULL> global variable

Added the <PROXYFULL> built-in variable: it resolves to the complete proxy string
host:port:user:pass (or just host:port when the proxy has no authentication). <PROXY> is
unchanged and still resolves to host:port only (OpenBullet convention). Use <PROXYFULL> to reuse the
authenticated proxy inside a config (scripts, a second hop, a header, etc.). It is a pure text
substitution in BlockBase.ReplaceValues and does not touch the request path — real proxy auth still
travels in the CProxy object (Username/Password), not in this string. Also registered in the
strict-variable-check built-in list so it is never flagged as undeclared.

Runner — "Completed" vs "Aborted" end-of-run message

The runner's job log said "Aborted Runner at …" even when a run finished naturally (all data tested).
It now distinguishes the two: a run stopped by the user (which passes through the Stopping state) logs
"Aborted Runner" in red, while a natural completion (Running→Idle directly) logs "Completed Runner" in
green.

Request block — SystemNet library now handles cookies correctly

The SystemNet HTTP library returned wrong results for cookie-dependent configs (e.g. a login that
shows its outcome via a session cookie set on a 302 redirect): the response came back as the pre-login
page with the flash message missing, so the KEYCHECK saw neither success nor fail. Cause: the shared
pooled handler used UseCookies=false (to be shareable) and sent cookies as a manual Cookie header,
so Set-Cookie issued during an automatic redirect was never applied to the followed request — the
session cookie was lost across the redirect.

  • SystemNet now uses a per-request HttpClientHandler with UseCookies=true and a per-request
    CookieContainer
    (populated from the bot's cookies), exactly like the RuriLib.Http path and OB2's
    HttpFactory.GetHttpClient. Cookies now follow redirects natively (incl. 302 Set-Cookie), and
    response cookies are read back from the container. Per-request container = no cross-bot leak.
  • Removed the now-unused shared handler pool (GetPooledHandler/_handlerPool) and dead _sharedClient.
    Cross-request connection pooling was never actually in effect for the other engines (OB2's
    HttpClientRequestHandler and the native RuriLib.Http path both create a handler per request), so
    dropping it changes no real pooling behavior while fixing cookie correctness.

Runner — ERROR retry cap + dedicated Errors counter (OB2 parity)

A connection ERROR (request timeout / connection reset-refused — i.e. the REQUEST block threw an
exception, set at LoliScript Status=ERROR) used to retry the same line forever with proxies off
(continue with no cap), so a slow/throttling target produced a retry avalanche that throttled it
further. Aligned to OpenBullet2's MultiRunJob:

  • ERROR is now treated like BAN for retries, capped by BanLoopEvasion. Within the cap (or cap
    0 = unlimited, same as OB2) the line is retried; past the cap it goes to ToCheck instead of
    looping. With proxies, the proxy is marked Bad (cooldown) as before.
  • New dedicated "Errors" counter (mirrors OB2 dataErrors): shown in the runner stats panel and
    incremented per ERROR attempt (thread-safe via Interlocked), instead of lumping errors into
    Retries. New i18n key Errors (en/it/es).
  • No double counting: an ERROR attempt bumps only Errors, not Retries (previously it bumped
    both, so the two counters advanced in lockstep). Retries is now reserved for BAN/RETRY outcomes,
    like OB2's Retried (which excludes errors). The per-line retry counter used for the evasion cap is
    unaffected.

Note: ERROR means a connection-level exception (no HTTP response). A bad HTTP status (403/429/500) is
not an ERROR — it returns a response and the KEYCHECK decides BAN/FAIL.

Runner — stats race fix: accurate FAIL/Total/CPM under load

Fixed non-deterministic under-counting of FAIL results (and therefore Total and CPM) when running
many bots. Measured on a local mock server (no network/proxy throttling): the native engine actually
sustained ~370 req/s, but the UI counter froze anywhere from 319 to ~9861 out of 10000 between runs,
with CPM showing 0 — the engine was fast, the display was wrong.

  • FailedList adds are now serialized. It is the only results list written off the UI thread: up
    to 100 bot threads called FailedList.Add(...) directly, while Hits/Custom/ToCheck always go through
    the UI dispatcher. ObservableCollection is not thread-safe, so concurrent adds lost items at random
    (FAILs are the bulk of results, so Total/CPM swung wildly). Adds, Count, the CPM CountRecent
    read and Clear now take a dedicated lock (AddFailResult). Microsecond hold, no UI marshalling —
    so no dispatcher flood / stutter. Covered by a 100k-concurrent-add test.

Runner — native engine speed: dedicated thread per bot

The native (LoliScript) engine was starved under load while the OB2 engine was not. Each bot blocks
sync-over-async on the HTTP call (RuriRequest → Task.Run(SendAsync).GetAwaiter().GetResult()), but
only OB2 bots were given a dedicated thread; native bots ran on the ThreadPool, which grows ~1
thread/500ms and also serves the HTTP continuations, so 100 blocked native bots throttled real
concurrency and CPM. Native bots now get a dedicated thread too (Legacy engine; the Pool engine
already uses its own dedicated threads). Same config, same target, proxy off: this was the measured
gap between the native and OB2 engines inside M2.

Runner — CPM fix: no more 1-second stall on failed requests

Fixed the main cause of the "accordion" CPM (oscillating 1↔300) and the overall slowdown versus
OB2 on the same target/proxies/bot count.

  • Removed the fixed Thread.Sleep(1000) on block errors. Every failed HTTP request (timeout,
    dead/slow proxy) raised a BlockProcessingException, and the runner's per-line loop slept a full
    second on it. With flaky proxies most bots spent their time inside that sleep, collapsing CPM in
    bursts — a penalty OB2 does not have. A failed BlockRequest already sets Status=ERROR, so the
    script ends on the next step with no spin-loop risk: the delay is now 0 ms. Generic (non-request)
    script errors keep a small 100 ms anti-spin guard, since they may not advance the script.
  • Delay logic extracted to a unit-tested helper (RunnerViewModel.BlockErrorBackoffMs).

Note: this path is shared by both runner engines (Legacy and continuous Pool), so the gain applies to
both. The continuous Pool setting only changes scheduling; it was never the bottleneck. Connection
reuse was ruled out as a lever: RuriLib.Http's ProxyClientHandler tears down and reopens the
TCP+TLS connection on every SendAsync (no internal pool), and OB2 uses the same handler — so reusing
it buys nothing.

Runner — strict variable check (pre-run validation)

  • Added optional "Strict variable check" (RL Settings → General, default off): when enabled, the Runner refuses to start if a config references variables that are never created by any block/command, listing the offending blocks. Fixes the legacy OB1 behavior where <UndeclaredVariable> was silently ignored.

Strict variable check — follow-up fixes

  • Custom cookies of REQUEST/TLSREQUEST are now scanned. COOKIE "name: <var>" lines populate the block's CustomCookiesList (a protected List<string>), which the reflection-based used-token scan skipped because it only walks public members. Since the runtime resolves those values through ReplaceValues, an undeclared <var> in a cookie now surfaces as an error instead of being missed (false negative closed).
  • A block's Label is no longer scanned for tokens. The used-token scan walked every writable public string, including BlockBase.Label, which is never passed to ReplaceValues at runtime. A <placeholder> written in a block's label no longer produces a spurious "undeclared variable" error (false positive closed).
  • The fail-soft guard now logs. The pre-run check is wrapped in a fail-soft catch so an internal validator error never blocks an otherwise valid start. The empty catch swallowed such errors silently; it now logs them (Runner colored log at Warning level, Stacker debugger via OB.Logger) while keeping the non-blocking behavior, so a skipped validation leaves a trace.

Runner — smoother result output in the colored job logger

Results printed in the runner's colored job logger no longer arrive in visible bursts (a batch of
lines freezing, then all appearing at once). The stutter came from the log rendering a control per
result:

  • Batched, single-pass rendering. The colored log added a TextBlock and called ScrollToBottom()
    on every single line, forcing a full layout pass per result; under a fast run these piled up and
    committed in one visual jump. It now enqueues entries lock-free and a DispatcherTimer flushes them
    ~10×/sec in one batch — many lines, one trim, one scroll — the same decoupled producer/consumer
    pattern already used by the app-level debug log. Per-result UI work on the dispatcher is now trivial
    (a list add plus an enqueue), so the heavy render no longer runs inline with each result.

Net effect: result throughput in the colored logger tracks the real work smoothly instead of jerking,
matching OpenBullet 2's fluidity. This is a UI-fluidity fix only; it does not change CPM, result
counts, ordering, or the job buffer size.

Runner — continuous scheduler (Pool engine, opt-in)

The runner now offers a second Runner engine option alongside the original Legacy behavior. The new Pool engine replaces the legacy poll loop with a continuous, fixed-size worker-pool scheduler that feeds results from a bounded queue with no fixed sleep, delivering smoother line-advancement throughout the run and eliminating the characteristic ~5 Hz pulsing observed under heavy load.

  • New "Runner engine" setting. Settings › General gains a dropdown to choose between Legacy (default,
    original behavior unchanged) and Pool (new continuous scheduler). The setting persists across sessions.
    Choose Legacy if you need to debug a bot issue or want the original polling behavior. Pool is opt-in;
    Legacy remains the default.

  • Pool scheduler: continuous worker threads instead of poll/sleep. The legacy engine polls for completed
    bots in a loop that sleeps 200 ms between checks, creating a sawtooth pattern: when bots saturate the
    queue, results pile up during each sleep cycle and burst out in a 5 Hz wave. The Pool engine runs a fixed
    set of long-lived worker threads that consume bots from a bounded queue and process them on demand, with
    no artificial sleep. This decouples line throughput from the poll interval: lines flow continuously as
    workers complete them.

  • Decoupled progress timer (both engines). Hit/Custom/ToCheck/Fail counters and the progress bar are
    now published on a fixed ~100 ms cadence (via a dedicated timer), independent of the per-result rendering
    rate. This ensures the counters advance smoothly instead of jumping in bursts. Both engines benefit: the
    legacy engine no longer jumps its counters every 200 ms, and the Pool engine advances them on a consistent
    tick even when individual results arrive at irregular rates.

  • Fixed bot count under Pool. When running under the Pool engine, the bot count is held constant for the
    duration of the run (the "Bots" slider in the UI is disabled until the run stops). This eliminates a class
    of race conditions in the pool orchestration and aligns with OpenBullet 2's behavior. You may still change
    the bot count while stopped, and it takes effect on the next run start.

Net effect: line throughput and counter advancement both remain smooth and continuous throughout the run,
matching OpenBullet 2. This is a UI-fluidity and scheduling improvement only; it does not change result
semantics — hit/fail/retry counts, proxy rotation, webhook/notification integrations, MaxHits/MaxCPM limits,
or the order of effects remain exactly as before. Legacy remains available as a fallback for debugging or
compatibility.

Updater — hardening against partial updates

Fixes the root cause of a report where, after updating (4.0.3 → 4.0.4 → 4.1.0), the Request
block disappeared and could no longer be added. The user had closed the updater window before it
finished, so the file replacement stopped halfway: a new executable ended up running against an
older RuriLib.dll from a previous release. The resulting binary mismatch broke the Request block
at runtime (its editor references members added in 4.1). This was never a bug in the block logic —
a clean 4.1 install works — but the updater made the torn state easy to reach and impossible to
detect.

  • Non-closable progress window. The external updater no longer runs as a bare console window
    the user could close mid-merge. It now shows a small, borderless progress window (current step +
    marquee bar) that cannot be closed by the user (Alt+F4 blocked) until the update finishes and
    closes it on its own. The user sees progress but can no longer interrupt the file replacement
    halfway — the exact way the partial update happened.
  • Startup integrity self-check. On launch the app now verifies that the loaded RuriLib.dll
    exposes the 4.1 API surface this build was compiled against. If files are inconsistent (a partial
    update), the app shows a clear "Incomplete update" message and refuses to start in a broken state
    instead of silently losing blocks. New localized strings Dialog_IncompleteUpdate(Title) (en/it/es).

Fix for affected users

If you already hit this: download and install the full 4.1.1 package again and let the updater
finish on its own (do not close any window). A clean install restores the Request block.

Startup — false "Incomplete update" on every build

The startup integrity self-check used type names that do not match the actual RuriLib assembly, so
Assembly.GetType(...) returned null and the app aborted with "Incomplete update" on a perfectly
consistent build.

  • RuriLib.Blocks.BlockRequest → the type is actually RuriLib.BlockRequest (the file lives in
    Blocks\ but declares namespace RuriLib).
  • RuriLib.Blocks.MultipartContent → actually RuriLib.Functions.Requests.MultipartContent.
  • One descriptor pointed at a type that lives in RuriLib.Http.dll, not RuriLib.dll, so it was
    never findable there; the check now relies on a BlockRequest member to prove the 4.1 API surface.

Corrected the required-API descriptor to the real names. Verified by metadata inspection: the old
names all resolve to null (the check always failed), the corrected names all resolve. The partial-update
protection still works — it now flags a genuinely stale RuriLib.dll, not a healthy build.

OPK configs — custom inputs are now saved and loaded

Fixes a bug where adding an entry in the Input tab of an imported OpenBullet2 (.opk)
config was lost on save: reopening the config showed no inputs. Any custom inputs already
present inside the imported .opk were also invisible in M2's Input tab.

Root cause: the OB2 bridge never mapped custom inputs in either direction. M2 keeps its own
input model (ConfigSettings.CustomInputs), while an OB2 config stores them separately in
InputSettings.CustomInputs inside the .opk. On import the OB2 inputs were not copied into
M2, and on save the dedicated OB2 save path (OB2Import.SaveEditedConfig → OB2Engine.Repack)
only re-packed the script plus name/author/icon/bots/proxy metadata — the inputs were dropped
before the .opk was written back.

  • Import. Custom inputs defined in an .opk now appear in M2's Input tab.
  • Save. Inputs added or edited in the Input tab are now written back into the .opk
    (InputSettings.CustomInputs) and persist across reloads and re-scans.
  • Bridging is done through a primitive DTO (OB2InputInfo) so the OB2 engine types are not
    exposed to M2, matching the existing metadata bridge.

OPK configs — custom inputs (input.NAME) now reach the run engine

An OB2 config can reference its custom inputs as input.NAME (exactly like data-line slices),
but M2's embedded OB2 engine built the input object only from the WordlistType slices — the custom
inputs were never passed to the run. So input.DNS (a custom input, not a slice) threw
"'input' does not contain a definition for 'DNS'" at runtime on every line.

  • Custom inputs are injected into input. OB2Engine.Run now accepts the config's custom
    inputs and adds them to the script's input object, using the values M2 already collects at run
    start (the AskCustomInputs prompt / their default answers). Slices keep precedence if a name
    collides.
  • Runner and debugger both pass them through. RunOB2Bot and the editor debugger forward the
    collected custom inputs to the engine, so input.NAME resolves for OB2 configs as it does in
    OpenBullet2.

Runner/debugger — WordlistType validation before the run

The runner now checks the selected WordlistType up front and refuses to start with an incompatible
one, so users are told immediately instead of running a whole job for nothing.

  • OB2 configs — name-based check (like OB2). The selected WordlistType must be one of the
    config's declared AllowedWordlistTypes. This is more robust than a regex check: different types
    (e.g. UserPass and Credentials) share the same regex ^.*:.*$, so regex alone cannot tell
    them apart — the name check can. If the config declares no allowed types, it falls back to a regex
    format check (blocks only when none of the first sampled lines match the type's regex).
  • The allowed-types list now survives clone and reload. Config.OB2AllowedWordlistTypes was
    lost when the runner cloned the config (and across the encrypted [OB2-META] round-trip), so the
    name check silently did nothing. It is now copied in IOManager clone and persisted/restored in
    the OB2 meta trailer.
  • The slice check excludes custom inputs and no longer hard-blocks on false positives.
    input.NAME may be a custom input rather than a data slice, and the old scan also matched inside
    comments/strings/disabled blocks. It now strips comments and strings, ignores custom-input names,
    and warns instead of refusing to start.
  • Native configs — same spirit, opt-in. A native config is validated the same way only if it
    declares AllowedWordlist1/2. Most native configs leave these empty, so their behavior is
    unchanged.

Select Wordlist — choose the WordlistType when importing from disk

Browsing an external file in the "Select Wordlist" dialog used to import it with the WordlistType
silently forced to Default, so a user:pass file could end up mistyped. The dialog now has an
inline Type combo: Browse fills the path and auto-detects the type from the first line (editable),
and Select imports the file with the chosen type. Picking an item from the collection list clears the
browsed path, so the list still takes precedence. Done inline — no extra pop-up dialog.

Environment — new WordlistTypes are picked up without a restart

Adding a WordlistType, keychain or export format from the Environment tool wrote the file but did
not reload the in-memory environment (OB.Settings.Environment, read only at startup). The new
entries stayed invisible to the runner and debugger until the app was restarted. The Environment
tool now reloads the environment right after writing, and the Stacker debugger's WordlistType combo
refreshes when the page is shown, so additions take effect immediately.

Official license avatar

Licenses can now carry an official user avatar, shown in the Home dashboard header and over the
shield icon on the About license card. It is cached locally after the first load and falls back
gracefully when no avatar is set.