Add Ehwrj clean-room live map
Some checks failed
build / build-test-publish (push) Has been cancelled
Some checks failed
build / build-test-publish (push) Has been cancelled
This commit is contained in:
11
tools/Ehwrj.Tools.Capture/Ehwrj.Tools.Capture.csproj
Normal file
11
tools/Ehwrj.Tools.Capture/Ehwrj.Tools.Capture.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Ehwrj.Tools.Capture</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/Ehwrj.Core/Ehwrj.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
202
tools/Ehwrj.Tools.Capture/Program.cs
Normal file
202
tools/Ehwrj.Tools.Capture/Program.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using Ehwrj.Core.Services;
|
||||
|
||||
namespace Ehwrj.Tools.Capture;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly Endpoint[] Endpoints =
|
||||
{
|
||||
new("map_info.json", "map_info.json", Required: true),
|
||||
new("map_obj.json", "map_obj.json", Required: true),
|
||||
new("map.img", "map.img", Required: true),
|
||||
new("state", "state.json", Required: false),
|
||||
new("hudmsg", "hudmsg.json", Required: false),
|
||||
new("gamechat", "gamechat.json", Required: false)
|
||||
};
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
var options = CaptureOptions.Parse(args);
|
||||
if (options.ShowHelp)
|
||||
{
|
||||
CaptureOptions.PrintHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.ValidateDirectory))
|
||||
{
|
||||
return WriteCaptureReport(options.ValidateDirectory);
|
||||
}
|
||||
|
||||
var captureDir = options.OutputDirectory ?? Path.Combine(
|
||||
"captures",
|
||||
DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture));
|
||||
|
||||
try
|
||||
{
|
||||
LoopbackGuard.EnsureLoopbackHttp(options.BaseAddress, "--base-url");
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(captureDir);
|
||||
|
||||
using var client = new HttpClient
|
||||
{
|
||||
BaseAddress = options.BaseAddress,
|
||||
Timeout = TimeSpan.FromMilliseconds(options.TimeoutMs)
|
||||
};
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("EhwrjCapture", "1.0"));
|
||||
|
||||
Console.WriteLine($"Capturing War Thunder local API from {client.BaseAddress}");
|
||||
Console.WriteLine($"Output: {Path.GetFullPath(captureDir)}");
|
||||
|
||||
var failures = 0;
|
||||
foreach (var endpoint in Endpoints)
|
||||
{
|
||||
var ok = await CaptureEndpointAsync(client, endpoint, captureDir).ConfigureAwait(false);
|
||||
if (!ok && endpoint.Required)
|
||||
{
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(captureDir, "README.txt"),
|
||||
$"""
|
||||
Ehwrj local API capture
|
||||
Captured at: {DateTimeOffset.Now:O}
|
||||
Source: {client.BaseAddress}
|
||||
|
||||
Files:
|
||||
- map_info.json
|
||||
- map_obj.json
|
||||
- map.img
|
||||
- state.json, hudmsg.json, gamechat.json when available
|
||||
|
||||
Use with:
|
||||
scripts/run-local-api-stub.sh 8111 {Path.GetFullPath(captureDir)}
|
||||
""");
|
||||
|
||||
WriteCaptureReport(captureDir);
|
||||
|
||||
if (failures > 0)
|
||||
{
|
||||
Console.Error.WriteLine($"{failures} required endpoint(s) failed.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine("Capture complete.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int WriteCaptureReport(string captureDir)
|
||||
{
|
||||
if (!Directory.Exists(captureDir))
|
||||
{
|
||||
Console.Error.WriteLine($"error: capture directory not found: {captureDir}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var report = CaptureFixtureAnalyzer.AnalyzeDirectory(captureDir);
|
||||
var reportPath = Path.Combine(report.Directory, "capture-report.txt");
|
||||
File.WriteAllText(reportPath, report.ToText());
|
||||
Console.WriteLine($"report: {reportPath}");
|
||||
|
||||
if (report.Warnings.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"{report.Warnings.Count} capture warning(s); see capture-report.txt.");
|
||||
}
|
||||
|
||||
return report.HasRequiredFiles ? 0 : 1;
|
||||
}
|
||||
|
||||
private static async Task<bool> CaptureEndpointAsync(HttpClient client, Endpoint endpoint, string captureDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await client.GetAsync(endpoint.Path).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var level = endpoint.Required ? "error" : "skip";
|
||||
Console.WriteLine($"{level}: {endpoint.Path} returned {(int)response.StatusCode}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
var outputPath = Path.Combine(captureDir, endpoint.FileName);
|
||||
await File.WriteAllBytesAsync(outputPath, bytes).ConfigureAwait(false);
|
||||
Console.WriteLine($"saved: {endpoint.Path} -> {outputPath} ({bytes.Length} bytes)");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||
{
|
||||
var level = endpoint.Required ? "error" : "skip";
|
||||
Console.WriteLine($"{level}: {endpoint.Path} failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record Endpoint(string Path, string FileName, bool Required);
|
||||
|
||||
internal sealed record CaptureOptions(Uri BaseAddress, string? OutputDirectory, string? ValidateDirectory, int TimeoutMs, bool ShowHelp)
|
||||
{
|
||||
public static CaptureOptions Parse(IReadOnlyList<string> args)
|
||||
{
|
||||
var baseAddress = new Uri("http://127.0.0.1:8111/");
|
||||
string? output = null;
|
||||
string? validate = null;
|
||||
var timeoutMs = 1200;
|
||||
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "-h":
|
||||
case "--help":
|
||||
return new CaptureOptions(baseAddress, output, validate, timeoutMs, ShowHelp: true);
|
||||
case "--base-url" when i + 1 < args.Count && Uri.TryCreate(args[++i], UriKind.Absolute, out var uri):
|
||||
baseAddress = uri;
|
||||
break;
|
||||
case "--out" when i + 1 < args.Count:
|
||||
output = args[++i];
|
||||
break;
|
||||
case "--validate" when i + 1 < args.Count:
|
||||
validate = args[++i];
|
||||
break;
|
||||
case "--timeout-ms" when i + 1 < args.Count && int.TryParse(args[++i], NumberStyles.None, CultureInfo.InvariantCulture, out var parsed):
|
||||
timeoutMs = Math.Clamp(parsed, 200, 10_000);
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Ignoring unknown argument: {args[i]}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new CaptureOptions(EnsureTrailingSlash(baseAddress), output, validate, timeoutMs, ShowHelp: false);
|
||||
}
|
||||
|
||||
public static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("""
|
||||
Usage:
|
||||
dotnet run --project tools/Ehwrj.Tools.Capture -- [--base-url http://127.0.0.1:8111/] [--out captures/name] [--timeout-ms 1200]
|
||||
dotnet run --project tools/Ehwrj.Tools.Capture -- --validate captures/name
|
||||
|
||||
Captures War Thunder local map, telemetry, and message endpoints into a fixture directory.
|
||||
Writes capture-report.txt with parser coverage and replay readiness.
|
||||
""");
|
||||
}
|
||||
|
||||
private static Uri EnsureTrailingSlash(Uri uri)
|
||||
{
|
||||
var text = uri.ToString();
|
||||
return text.EndsWith('/') ? uri : new Uri($"{text}/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Ehwrj.Tools.LocalApiStub</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/Ehwrj.Core/Ehwrj.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
412
tools/Ehwrj.Tools.LocalApiStub/Program.cs
Normal file
412
tools/Ehwrj.Tools.LocalApiStub/Program.cs
Normal file
@@ -0,0 +1,412 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Ehwrj.Core.Services;
|
||||
|
||||
namespace Ehwrj.Tools.LocalApiStub;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
var options = StubOptions.Parse(args);
|
||||
if (options.ShowHelp)
|
||||
{
|
||||
StubOptions.PrintHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!LoopbackGuard.IsLoopbackHost(options.Host))
|
||||
{
|
||||
Console.Error.WriteLine("Only loopback hosts are allowed for the local API stub.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var listener = new HttpListener();
|
||||
var prefix = $"http://{options.Host}:{options.Port}/";
|
||||
listener.Prefixes.Add(prefix);
|
||||
|
||||
using var shutdown = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
shutdown.Cancel();
|
||||
listener.Stop();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
}
|
||||
catch (HttpListenerException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Failed to listen on {prefix}: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Ehwrj local API stub listening on {prefix}");
|
||||
if (options.FixtureDirectory is not null)
|
||||
{
|
||||
Console.WriteLine($"Fixture replay: {Path.GetFullPath(options.FixtureDirectory)}");
|
||||
}
|
||||
Console.WriteLine("Endpoints: /map_info.json /map_obj.json /map.img /state /hudmsg /gamechat");
|
||||
Console.WriteLine("Press Ctrl+C to stop.");
|
||||
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
while (!shutdown.IsCancellationRequested)
|
||||
{
|
||||
HttpListenerContext context;
|
||||
try
|
||||
{
|
||||
context = await listener.GetContextAsync().WaitAsync(shutdown.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (HttpListenerException) when (shutdown.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => HandleAsync(context, startedAt, options), shutdown.Token);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task HandleAsync(HttpListenerContext context, DateTimeOffset startedAt, StubOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = context.Request.Url?.AbsolutePath ?? "/";
|
||||
if (await TryWriteFixtureAsync(context, path, options.FixtureDirectory).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (path)
|
||||
{
|
||||
case "/map_info.json":
|
||||
await WriteJsonAsync(context, CreateMapInfo()).ConfigureAwait(false);
|
||||
break;
|
||||
case "/map_obj.json":
|
||||
await WriteJsonAsync(context, CreateMapObjects((DateTimeOffset.UtcNow - startedAt).TotalSeconds)).ConfigureAwait(false);
|
||||
break;
|
||||
case "/map.img":
|
||||
await WriteBytesAsync(context, "image/bmp", MapImageGenerator.CreateBmp(512, 512)).ConfigureAwait(false);
|
||||
break;
|
||||
case "/state":
|
||||
await WriteJsonAsync(context, CreateState((DateTimeOffset.UtcNow - startedAt).TotalSeconds)).ConfigureAwait(false);
|
||||
break;
|
||||
case "/hudmsg":
|
||||
await WriteJsonAsync(context, CreateHudMessages((DateTimeOffset.UtcNow - startedAt).TotalSeconds)).ConfigureAwait(false);
|
||||
break;
|
||||
case "/gamechat":
|
||||
await WriteJsonAsync(context, CreateGameChat((DateTimeOffset.UtcNow - startedAt).TotalSeconds)).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
await WriteTextAsync(context, "Ehwrj local API stub\n").ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
await WriteTextAsync(context, ex.Message).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Response.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private static object CreateMapInfo()
|
||||
{
|
||||
return new
|
||||
{
|
||||
grid_size = new[] { 1.0, 1.0 },
|
||||
grid_steps = new[] { 0.1, 0.1 },
|
||||
min_x = 0.0,
|
||||
max_x = 1.0,
|
||||
min_y = 0.0,
|
||||
max_y = 1.0
|
||||
};
|
||||
}
|
||||
|
||||
private static object[] CreateMapObjects(double seconds)
|
||||
{
|
||||
var phase = seconds * 0.18;
|
||||
var playerX = 0.5 + Math.Cos(phase) * 0.08;
|
||||
var playerY = 0.5 + Math.Sin(phase) * 0.08;
|
||||
|
||||
return new object[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = "player",
|
||||
name = "Player",
|
||||
type = "player_aircraft",
|
||||
team = "player",
|
||||
icon = "fighter",
|
||||
x = Round(playerX),
|
||||
y = Round(playerY),
|
||||
dx = Round(Math.Cos(phase)),
|
||||
dy = Round(Math.Sin(phase))
|
||||
},
|
||||
new
|
||||
{
|
||||
id = "bandit-1",
|
||||
name = "Bandit 1",
|
||||
type = "aircraft",
|
||||
team = "enemy",
|
||||
icon = "fighter",
|
||||
x = Round(0.5 + Math.Cos(phase + 1.8) * 0.22),
|
||||
y = Round(0.5 + Math.Sin(phase + 1.8) * 0.18)
|
||||
},
|
||||
new
|
||||
{
|
||||
id = "bandit-2",
|
||||
name = "Bandit 2",
|
||||
type = "aircraft",
|
||||
team = "enemy",
|
||||
icon = "bomber",
|
||||
x = Round(0.5 + Math.Cos(phase * 0.7 + 3.2) * 0.34),
|
||||
y = Round(0.5 + Math.Sin(phase * 0.7 + 3.2) * 0.25)
|
||||
},
|
||||
new
|
||||
{
|
||||
id = "objective-a",
|
||||
name = "A",
|
||||
type = "capture_zone",
|
||||
team = "ally",
|
||||
icon = "zone",
|
||||
x = 0.25,
|
||||
y = 0.28
|
||||
},
|
||||
new
|
||||
{
|
||||
id = "objective-b",
|
||||
name = "B",
|
||||
type = "bombing_point",
|
||||
team = "enemy",
|
||||
icon = "zone",
|
||||
x = 0.72,
|
||||
y = 0.68
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object CreateState(double seconds)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["TAS, km/h"] = Math.Round(720 + Math.Sin(seconds * 0.25) * 80, 1),
|
||||
["IAS, km/h"] = Math.Round(650 + Math.Sin(seconds * 0.22) * 65, 1),
|
||||
["Vy, m/s"] = Math.Round(Math.Sin(seconds * 0.34) * 38, 2),
|
||||
["H, m"] = Math.Round(2200 + Math.Sin(seconds * 0.11) * 420, 1)
|
||||
};
|
||||
}
|
||||
|
||||
private static object CreateHudMessages(double seconds)
|
||||
{
|
||||
var wave = (int)(seconds / 12) % 4;
|
||||
return new
|
||||
{
|
||||
events = new object[]
|
||||
{
|
||||
new { text = wave == 0 ? "Enemy aircraft spotted" : "Maintain formation", enemy = wave == 0 },
|
||||
new { text = "Objective B under attack", enemy = true }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object CreateGameChat(double seconds)
|
||||
{
|
||||
var wave = (int)(seconds / 15) % 3;
|
||||
return new
|
||||
{
|
||||
items = new object[]
|
||||
{
|
||||
new { text = wave == 0 ? "Cover the bomber route" : "Regroup near A", enemy = false }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static double Round(double value)
|
||||
{
|
||||
return Math.Round(Math.Clamp(value, 0.02, 0.98), 5);
|
||||
}
|
||||
|
||||
private static Task WriteJsonAsync(HttpListenerContext context, object value)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, JsonOptions);
|
||||
return WriteBytesAsync(context, "application/json", Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
|
||||
private static Task WriteTextAsync(HttpListenerContext context, string text)
|
||||
{
|
||||
return WriteBytesAsync(context, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
private static async Task WriteBytesAsync(HttpListenerContext context, string contentType, byte[] bytes)
|
||||
{
|
||||
context.Response.ContentType = contentType;
|
||||
context.Response.ContentLength64 = bytes.Length;
|
||||
await context.Response.OutputStream.WriteAsync(bytes).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<bool> TryWriteFixtureAsync(HttpListenerContext context, string path, string? fixtureDirectory)
|
||||
{
|
||||
if (fixtureDirectory is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fileName = path switch
|
||||
{
|
||||
"/map_info.json" => "map_info.json",
|
||||
"/map_obj.json" => "map_obj.json",
|
||||
"/map.img" => "map.img",
|
||||
"/state" => "state.json",
|
||||
"/hudmsg" => "hudmsg.json",
|
||||
"/gamechat" => "gamechat.json",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (fileName is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(fixtureDirectory, fileName);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false);
|
||||
var contentType = fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
? "application/json"
|
||||
: "image/bmp";
|
||||
await WriteBytesAsync(context, contentType, bytes).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record StubOptions(string Host, int Port, string? FixtureDirectory, bool ShowHelp)
|
||||
{
|
||||
public static StubOptions Parse(IReadOnlyList<string> args)
|
||||
{
|
||||
var host = "127.0.0.1";
|
||||
var port = 8111;
|
||||
string? fixtureDirectory = null;
|
||||
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "-h":
|
||||
case "--help":
|
||||
return new StubOptions(host, port, fixtureDirectory, true);
|
||||
case "--host" when i + 1 < args.Count:
|
||||
host = args[++i];
|
||||
break;
|
||||
case "--port" when i + 1 < args.Count && int.TryParse(args[++i], NumberStyles.None, CultureInfo.InvariantCulture, out var parsed):
|
||||
port = parsed;
|
||||
break;
|
||||
case "--fixture-dir" when i + 1 < args.Count:
|
||||
fixtureDirectory = args[++i];
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Ignoring unknown argument: {args[i]}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new StubOptions(host, port, fixtureDirectory, false);
|
||||
}
|
||||
|
||||
public static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("""
|
||||
Usage:
|
||||
dotnet run --project tools/Ehwrj.Tools.LocalApiStub -- [--host 127.0.0.1] [--port 8111] [--fixture-dir captures/name]
|
||||
|
||||
Serves deterministic War Thunder-like local map, telemetry, and message endpoints for UI development.
|
||||
When --fixture-dir is supplied, files captured by Ehwrj.Tools.Capture are replayed first.
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
internal static class MapImageGenerator
|
||||
{
|
||||
public static byte[] CreateBmp(int width, int height)
|
||||
{
|
||||
var stride = ((width * 3 + 3) / 4) * 4;
|
||||
var pixelBytes = stride * height;
|
||||
var fileSize = 54 + pixelBytes;
|
||||
var bytes = new byte[fileSize];
|
||||
|
||||
bytes[0] = (byte)'B';
|
||||
bytes[1] = (byte)'M';
|
||||
WriteInt32(bytes, 2, fileSize);
|
||||
WriteInt32(bytes, 10, 54);
|
||||
WriteInt32(bytes, 14, 40);
|
||||
WriteInt32(bytes, 18, width);
|
||||
WriteInt32(bytes, 22, height);
|
||||
WriteInt16(bytes, 26, 1);
|
||||
WriteInt16(bytes, 28, 24);
|
||||
WriteInt32(bytes, 34, pixelBytes);
|
||||
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var sourceY = height - 1 - y;
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var nx = x / (double)Math.Max(1, width - 1);
|
||||
var ny = sourceY / (double)Math.Max(1, height - 1);
|
||||
var grid = x % 64 == 0 || sourceY % 64 == 0;
|
||||
var water = Math.Sin(nx * 8.0) + Math.Cos(ny * 7.0) > 1.15;
|
||||
|
||||
var r = water ? (byte)28 : (byte)(42 + ny * 34);
|
||||
var g = water ? (byte)78 : (byte)(74 + nx * 46);
|
||||
var b = water ? (byte)98 : (byte)(58 + (1 - ny) * 34);
|
||||
|
||||
if (grid)
|
||||
{
|
||||
r = (byte)Math.Min(255, r + 38);
|
||||
g = (byte)Math.Min(255, g + 38);
|
||||
b = (byte)Math.Min(255, b + 38);
|
||||
}
|
||||
|
||||
var offset = 54 + y * stride + x * 3;
|
||||
bytes[offset] = b;
|
||||
bytes[offset + 1] = g;
|
||||
bytes[offset + 2] = r;
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void WriteInt16(byte[] bytes, int offset, short value)
|
||||
{
|
||||
bytes[offset] = (byte)value;
|
||||
bytes[offset + 1] = (byte)(value >> 8);
|
||||
}
|
||||
|
||||
private static void WriteInt32(byte[] bytes, int offset, int value)
|
||||
{
|
||||
bytes[offset] = (byte)value;
|
||||
bytes[offset + 1] = (byte)(value >> 8);
|
||||
bytes[offset + 2] = (byte)(value >> 16);
|
||||
bytes[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user