Table of Contents

Utilities

This page covers the helper classes provided by Hi3Helper.Plugin.Core that you will use most frequently when writing a plugin.

Table of contents

  1. PluginHttpClientBuilder
  2. RetryableCopyToStreamTask
  3. SpeedLimiterService
  4. PluginDisposableMemory<T>

1. PluginHttpClientBuilder

PluginHttpClientBuilder is a fluent builder for System.Net.Http.HttpClient. Every HttpClient you create in a plugin should be created through this builder, because it:

  • Automatically routes all connections through the launcher's DNS resolver callback (both synchronous and asynchronous) when one has been installed by the launcher via SetDnsResolverCallback / SetDnsResolverCallbackAsync.
  • Applies the proxy settings pushed by the launcher via IPlugin.SetPluginProxySettings.
  • Sets a plugin-specific default User-Agent header.
  • Uses HTTP/3 by default (falls back to HTTP/2 in MANUALCOM mode).

Basic usage

using Hi3Helper.Plugin.Core.Utility;
using System.Net.Http;

// Minimal — all defaults
HttpClient client = new PluginHttpClientBuilder()
    .Create();

// Customised
HttpClient client = new PluginHttpClientBuilder()
    .SetBaseUrl("https://api.example.com/")
    .SetTimeout(30)                         // seconds
    .SetMaxConnection(16)
    .AllowRedirections()
    .AddHeader("X-My-Header", "value")
    .Create();

Fluent API reference

Method Default Description
SetBaseUrl(string\|Uri) Sets HttpClient.BaseAddress
SetTimeout(double seconds) 90 s Request timeout
SetMaxConnection(int) 32 SocketsHttpHandler.MaxConnectionsPerServer
SetHttpVersion(Version?, HttpVersionPolicy) HTTP/3 (HTTP/2 in MANUALCOM) Negotiated HTTP protocol version
SetAllowedDecompression(DecompressionMethods) All Response decompression
AllowRedirections(bool) true Follow 3xx responses
AllowCookies(bool) true Cookie store
AllowUntrustedCert(bool) false Skip TLS certificate validation
SetUserAgent(string?) Auto-generated User-Agent header
SetAuthHeader(string?) null Authorization header
AddHeader(string, string?) Arbitrary request header
Create() Builds and returns the configured HttpClient
Note

The DNS resolver callback and proxy wiring are set up inside Create() automatically. You never need to configure them manually — just make sure the launcher has called SetDnsResolverCallback before the plugin starts making requests.


2. RetryableCopyToStreamTask

RetryableCopyToStreamTask runs a copy-to operation from a source Stream to a target Stream with automatic retry on network errors, per-read timeout, and optional speed throttling. It is ideal for all game download tasks.

Creating a task

using Hi3Helper.Plugin.Core.Utility;
using System.IO;
using System.Net.Http;

// Source factory — called again on each retry with the last successfully
// written byte position so the download can be resumed.
RetryableCopyToStreamTask.SourceStreamFactory factory = async (lastPosition, token) =>
{
    HttpRequestMessage request = new(HttpMethod.Get, "https://cdn.example.com/game.zip");
    if (lastPosition > 0)
        request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(lastPosition, null);

    HttpResponseMessage response = await _httpClient.SendAsync(
        request, HttpCompletionOption.ResponseHeadersRead, token);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStreamAsync(token);
};

FileStream output = File.OpenWrite("game.zip");

RetryableCopyToStreamTask task = RetryableCopyToStreamTask.CreateTask(
    factory,
    output,
    new RetryableCopyToStreamTaskOptions
    {
        MaxBufferSize     = 65_536,  // 64 KiB read buffer
        MaxRetryCount     = 5,
        MaxTimeoutSeconds = 10,
        RetryDelaySeconds = 1,
        // Optionally wire in speed throttling (see section 3):
        SpeedLimiterServiceContext = SpeedLimiterService.CreateServiceContext()
    });

await task.StartTaskAsync(
    readDelegate: bytesRead => _bytesDownloaded += bytesRead,
    token: cancellationToken);

RetryableCopyToStreamTaskOptions reference

Property Default Description
MaxBufferSize 4 096 bytes Read-write buffer size (clamped: 1 KiB – 1 MiB)
MaxRetryCount 5 Maximum number of retry attempts
MaxTimeoutSeconds 10 s Per-read/write timeout (minimum 2 s)
RetryDelaySeconds 1 s Pause between retry attempts
IsDisposeTargetStream false Dispose the target stream when the task is disposed
SpeedLimiterServiceContext nint.Zero Handle from SpeedLimiterService.CreateServiceContext()

3. SpeedLimiterService

SpeedLimiterService integrates with the launcher's RegisterSpeedThrottlerService export to enforce a per-session download speed cap. The launcher supplies the token-bucket parameters; the plugin calls AddBytesOrWaitAsync after each chunk to yield when the budget is exhausted.

Usage

using Hi3Helper.Plugin.Core.Utility;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

async Task DownloadAsync(Stream source, Stream dest, CancellationToken token)
{
    // Create one context per concurrent download stream.
    nint ctx = SpeedLimiterService.CreateServiceContext();

    byte[] buffer = new byte[65_536];
    int read;

    while ((read = await source.ReadAsync(buffer, token)) > 0)
    {
        await dest.WriteAsync(buffer.AsMemory(0, read), token);

        // Throttle: waits until the launcher's token bucket replenishes.
        await SpeedLimiterService.AddBytesOrWaitAsync(ctx, read, token);
    }
}
Note

If the launcher has not called RegisterSpeedThrottlerService, AddBytesOrWaitAsync returns immediately without any delay, so the code works correctly in both throttled and unthrottled environments.

RetryableCopyToStreamTask integrates SpeedLimiterService natively via RetryableCopyToStreamTaskOptions.SpeedLimiterServiceContext.


4. PluginDisposableMemory<T>

PluginDisposableMemory<T> is an unmanaged memory wrapper that can be passed across the COM boundary between the plugin and the launcher. It pairs a raw T* pointer with its length and a disposability flag, and exposes a managed Span<T> view.

Allocating memory

using Hi3Helper.Plugin.Core;
using Hi3Helper.Plugin.Core.Utility;

// Allocate 128 ints in unmanaged memory
using PluginDisposableMemory<int> memory = PluginDisposableMemory<int>.Allocate(128);

// Write via Span<T>
Span<int> span = memory.AsSpan();
for (int i = 0; i < span.Length; i++)
    span[i] = i * 2;

// Index access
ref int first = ref memory[0]; // ref-based, bounds-checked

// Stream view
using UnmanagedMemoryStream stream = memory.AsStream();

API reference

Member Description
PluginDisposableMemory<T>.Allocate(int count) Allocates count elements of T in unmanaged memory
PluginDisposableMemory<T>.Empty A zero-length non-disposable sentinel value
Length Number of elements
IsEmpty true when Length == 0
this[int] Bounds-checked ref access to an element
AsSpan(int offset, int length) Managed Span<T> view over a range
AsPointer() Raw T* pointer
AsSafePointer() nint representation of the pointer
AsStream() UnmanagedMemoryStream over the entire buffer
Dispose() Frees the unmanaged memory (only if IsDisposable == true)
ForceDispose() Frees regardless of the disposability flag

Receiving memory from the launcher

The launcher may pass memory to the plugin via PluginDisposableMemoryMarshal. Use the extension method to convert it into a typed span:

using Hi3Helper.Plugin.Core;

// 'marshal' comes from the launcher via COM
PluginDisposableMemory<byte> data = marshal.ToManagedSpan<byte>();
try
{
    // use data...
}
finally
{
    data.Dispose(); // frees the unmanaged pointer if the launcher marked it as disposable
}