Repository navigation
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
HttpClientHandlerwithUseCookies=trueand 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. 302Set-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
HttpClientRequestHandlerand 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 viaInterlocked), instead of lumping errors into
Retries. New i18n keyErrors(en/it/es). - No double counting: an ERROR attempt bumps only
Errors, notRetries(previously it bumped
both, so the two counters advanced in lockstep).Retriesis now reserved for BAN/RETRY outcomes,
like OB2'sRetried(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.
FailedListadds are now serialized. It is the only results list written off the UI thread: up
to 100 bot threads calledFailedList.Add(...)directly, while Hits/Custom/ToCheck always go through
the UI dispatcher.ObservableCollectionis not thread-safe, so concurrent adds lost items at random
(FAILs are the bulk of results, so Total/CPM swung wildly). Adds,Count, the CPMCountRecent
read andClearnow 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 aBlockProcessingException, 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 failedBlockRequestalready setsStatus=ERROR, so the
script ends on the next step with no spin-loop risk: the delay is now0 ms. Generic (non-request)
script errors keep a small100 msanti-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'sCustomCookiesList(aprotected List<string>), which the reflection-based used-token scan skipped because it only walks public members. Since the runtime resolves those values throughReplaceValues, 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 toReplaceValuesat 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
catchso an internal validator error never blocks an otherwise valid start. The emptycatchswallowed such errors silently; it now logs them (Runner colored log at Warning level, Stacker debugger viaOB.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
TextBlockand calledScrollToBottom()
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 aDispatcherTimerflushes 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) andPool(new continuous scheduler). The setting persists across sessions.
ChooseLegacyif you need to debug a bot issue or want the original polling behavior.Poolis opt-in;
Legacyremains 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 stringsDialog_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 actuallyRuriLib.BlockRequest(the file lives in
Blocks\but declaresnamespace RuriLib).RuriLib.Blocks.MultipartContent→ actuallyRuriLib.Functions.Requests.MultipartContent.- One descriptor pointed at a type that lives in
RuriLib.Http.dll, notRuriLib.dll, so it was
never findable there; the check now relies on aBlockRequestmember 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
.opknow 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.Runnow accepts the config's custom
inputs and adds them to the script'sinputobject, 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.
RunOB2Botand the editor debugger forward the
collected custom inputs to the engine, soinput.NAMEresolves 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 declaredAllowedWordlistTypes. This is more robust than a regex check: different types
(e.g.UserPassandCredentials) 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.OB2AllowedWordlistTypeswas
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 inIOManagerclone and persisted/restored in
the OB2 meta trailer. - The slice check excludes custom inputs and no longer hard-blocks on false positives.
input.NAMEmay 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
declaresAllowedWordlist1/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.