Danro-Jump/Assets/Scripts/Runtime/Prizes/Qr/QrCodeByteModeEncoder.cs
Danil Iarosh c1b72854f4 Implement prize QR and service UI polish
Подробные изменения:

- Добавлена система призовых уровней и выдачи призов с QR-контуром Telegram, шаблоном сообщения и генерацией QR без внешних библиотек.

- Доработаны сервисные настройки: действия кнопок, модальные окна, ввод с джойстика/клавиатуры, статистика, настройки призов и маршрутизация QR.

- Переработаны gameplay HUD и overlay-панели на UI Toolkit со спрайтовыми плашками, DEV BUILD-статистикой, экраном продолжения и экраном результата приза.

- Исправлены сценарии смерти, GameOver, продолжения игры, начисления очков за высоту и перехода в главное меню.

- Перенесены UI-шаблоны из Resources в Assets/UI, подключена Addressables-загрузка шаблонов настроек и standalone-стили для MainMenuModals.

- Добавлены EditMode/PlayMode тесты для призов, QR, arcade input, UI-модалок, scene flow, HUD и gameplay-сессии.

- Обновлены сцены, настройки Addressables, service settings database, документация и дорожная карта.

Проверки:

- Targeted EditMode QR/UI/settings: 23/23 passed

- Targeted UI templates/EditMode: 17/17 passed

- MainMenuUxmlTests: 3/3 passed

- Full EditMode: 129/129 passed

- PlayMode: 6/6 passed

- Runtime Addressables template load check: loaded=5

- Unity Console после финального refresh: 0 errors
2026-06-12 16:43:28 +03:00

381 lines
12 KiB
C#

using System.Collections.Generic;
using System.Text;
namespace DanroJump.Prizes.Qr
{
/// <summary>
/// Минимальный QR encoder: byte mode, ECC Low, версии 1-5.
/// </summary>
public static class QrCodeByteModeEncoder
{
private static readonly VersionInfo[] Versions =
{
new(1, 19, 7),
new(2, 34, 10),
new(3, 55, 15),
new(4, 80, 20),
new(5, 108, 26),
};
private static readonly int[] ExpTable = new int[512];
private static readonly int[] LogTable = new int[256];
static QrCodeByteModeEncoder()
{
var value = 1;
for (var index = 0; index < 255; index++)
{
ExpTable[index] = value;
LogTable[value] = index;
value <<= 1;
if ((value & 0x100) != 0)
{
value ^= 0x11D;
}
}
for (var index = 255; index < ExpTable.Length; index++)
{
ExpTable[index] = ExpTable[index - 255];
}
}
public static bool TryEncode(string text, out QrCodeMatrix matrix, out string error)
{
matrix = null;
var data = Encoding.UTF8.GetBytes(text ?? string.Empty);
if (!TrySelectVersion(data.Length, out var version))
{
error = "QR-ссылка слишком длинная для базового QR-генератора.";
return false;
}
var dataCodewords = BuildDataCodewords(data, version.DataCodewords);
var eccCodewords = CalculateErrorCorrection(dataCodewords, version.ErrorCorrectionCodewords);
var allCodewords = new List<int>(dataCodewords.Count + eccCodewords.Length);
allCodewords.AddRange(dataCodewords);
allCodewords.AddRange(eccCodewords);
matrix = DrawMatrix(version.Version, allCodewords);
error = string.Empty;
return true;
}
private static bool TrySelectVersion(int byteLength, out VersionInfo version)
{
foreach (var candidate in Versions)
{
if (4 + 8 + byteLength * 8 <= candidate.DataCodewords * 8)
{
version = candidate;
return true;
}
}
version = default;
return false;
}
private static List<int> BuildDataCodewords(byte[] data, int capacityCodewords)
{
var bits = new List<int>(capacityCodewords * 8);
AppendBits(bits, 0b0100, 4);
AppendBits(bits, data.Length, 8);
foreach (var value in data)
{
AppendBits(bits, value, 8);
}
var capacityBits = capacityCodewords * 8;
AppendBits(bits, 0, System.Math.Min(4, capacityBits - bits.Count));
while (bits.Count % 8 != 0)
{
bits.Add(0);
}
var codewords = new List<int>(capacityCodewords);
for (var index = 0; index < bits.Count; index += 8)
{
var value = 0;
for (var bit = 0; bit < 8; bit++)
{
value = (value << 1) | bits[index + bit];
}
codewords.Add(value);
}
var pad = 0;
while (codewords.Count < capacityCodewords)
{
codewords.Add((pad++ & 1) == 0 ? 0xEC : 0x11);
}
return codewords;
}
private static void AppendBits(List<int> bits, int value, int count)
{
for (var index = count - 1; index >= 0; index--)
{
bits.Add((value >> index) & 1);
}
}
private static int[] CalculateErrorCorrection(IReadOnlyList<int> data, int degree)
{
var generator = BuildGeneratorPolynomial(degree);
var result = new int[degree];
foreach (var value in data)
{
var factor = value ^ result[0];
for (var index = 0; index < degree - 1; index++)
{
result[index] = result[index + 1];
}
result[degree - 1] = 0;
for (var index = 0; index < degree; index++)
{
result[index] ^= Multiply(generator[index + 1], factor);
}
}
return result;
}
private static int[] BuildGeneratorPolynomial(int degree)
{
var result = new[] { 1 };
for (var index = 0; index < degree; index++)
{
var next = new int[result.Length + 1];
for (var coefficient = 0; coefficient < result.Length; coefficient++)
{
next[coefficient] ^= result[coefficient];
next[coefficient + 1] ^= Multiply(result[coefficient], ExpTable[index]);
}
result = next;
}
return result;
}
private static int Multiply(int left, int right)
{
return left == 0 || right == 0 ? 0 : ExpTable[LogTable[left] + LogTable[right]];
}
private static QrCodeMatrix DrawMatrix(int version, IReadOnlyList<int> codewords)
{
var size = version * 4 + 17;
var matrix = new QrCodeMatrix(size);
var function = new bool[size, size];
DrawFinder(matrix, function, 0, 0);
DrawFinder(matrix, function, size - 7, 0);
DrawFinder(matrix, function, 0, size - 7);
DrawTiming(matrix, function);
DrawAlignment(matrix, function, version);
matrix.SetModule(8, size - 8, true);
function[size - 8, 8] = true;
ReserveFormat(function, size);
DrawData(matrix, function, codewords);
DrawFormat(matrix);
return matrix;
}
private static void DrawFinder(QrCodeMatrix matrix, bool[,] function, int x, int y)
{
for (var dy = -1; dy <= 7; dy++)
{
for (var dx = -1; dx <= 7; dx++)
{
var xx = x + dx;
var yy = y + dy;
if (xx < 0 || yy < 0 || xx >= matrix.Size || yy >= matrix.Size)
{
continue;
}
var inside = dx >= 0 && dx <= 6 && dy >= 0 && dy <= 6;
var dark = inside &&
(dx == 0 || dx == 6 || dy == 0 || dy == 6 ||
dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4);
matrix.SetModule(xx, yy, dark);
function[yy, xx] = true;
}
}
}
private static void DrawTiming(QrCodeMatrix matrix, bool[,] function)
{
for (var index = 8; index < matrix.Size - 8; index++)
{
var dark = (index & 1) == 0;
matrix.SetModule(index, 6, dark);
matrix.SetModule(6, index, dark);
function[6, index] = true;
function[index, 6] = true;
}
}
private static void DrawAlignment(QrCodeMatrix matrix, bool[,] function, int version)
{
if (version <= 1)
{
return;
}
var positions = new[] { 6, matrix.Size - 7 };
foreach (var y in positions)
{
foreach (var x in positions)
{
if (function[y, x])
{
continue;
}
for (var dy = -2; dy <= 2; dy++)
{
for (var dx = -2; dx <= 2; dx++)
{
var distance = System.Math.Max(System.Math.Abs(dx), System.Math.Abs(dy));
matrix.SetModule(x + dx, y + dy, distance != 1);
function[y + dy, x + dx] = true;
}
}
}
}
}
private static void ReserveFormat(bool[,] function, int size)
{
for (var index = 0; index < 9; index++)
{
if (index != 6)
{
function[8, index] = true;
function[index, 8] = true;
}
}
for (var index = 0; index < 8; index++)
{
function[8, size - 1 - index] = true;
function[size - 1 - index, 8] = true;
}
}
private static void DrawData(QrCodeMatrix matrix, bool[,] function, IReadOnlyList<int> codewords)
{
var bits = new List<int>(codewords.Count * 8);
foreach (var value in codewords)
{
AppendBits(bits, value, 8);
}
var bitIndex = 0;
var upward = true;
for (var right = matrix.Size - 1; right >= 1; right -= 2)
{
if (right == 6)
{
right--;
}
for (var vertical = 0; vertical < matrix.Size; vertical++)
{
var y = upward ? matrix.Size - 1 - vertical : vertical;
for (var column = 0; column < 2; column++)
{
var x = right - column;
if (function[y, x])
{
continue;
}
var dark = bitIndex < bits.Count && bits[bitIndex++] != 0;
if (((x + y) & 1) == 0)
{
dark = !dark;
}
matrix.SetModule(x, y, dark);
}
}
upward = !upward;
}
}
private static void DrawFormat(QrCodeMatrix matrix)
{
var bits = CalculateFormatBits();
for (var index = 0; index <= 5; index++)
{
matrix.SetModule(8, index, GetBit(bits, index));
}
matrix.SetModule(8, 7, GetBit(bits, 6));
matrix.SetModule(8, 8, GetBit(bits, 7));
matrix.SetModule(7, 8, GetBit(bits, 8));
for (var index = 9; index < 15; index++)
{
matrix.SetModule(14 - index, 8, GetBit(bits, index));
}
for (var index = 0; index < 8; index++)
{
matrix.SetModule(matrix.Size - 1 - index, 8, GetBit(bits, index));
}
for (var index = 8; index < 15; index++)
{
matrix.SetModule(8, matrix.Size - 15 + index, GetBit(bits, index));
}
matrix.SetModule(8, matrix.Size - 8, true);
}
private static int CalculateFormatBits()
{
const int errorCorrectionLow = 1;
const int mask = 0;
const int generator = 0x537;
var data = (errorCorrectionLow << 3) | mask;
var value = data << 10;
for (var bit = 14; bit >= 10; bit--)
{
if (((value >> bit) & 1) != 0)
{
value ^= generator << (bit - 10);
}
}
return ((data << 10) | value) ^ 0x5412;
}
private static bool GetBit(int value, int index)
{
return ((value >> index) & 1) != 0;
}
private readonly struct VersionInfo
{
public VersionInfo(int version, int dataCodewords, int errorCorrectionCodewords)
{
Version = version;
DataCodewords = dataCodewords;
ErrorCorrectionCodewords = errorCorrectionCodewords;
}
public int Version { get; }
public int DataCodewords { get; }
public int ErrorCorrectionCodewords { get; }
}
}
}