codex-5.2: перестройка Client/AssetsManager
This commit is contained in:
484
Src/Client/AssetsHeaderCodec.cpp
Normal file
484
Src/Client/AssetsHeaderCodec.cpp
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
#include "Client/AssetsHeaderCodec.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include "TOSLib.hpp"
|
||||||
|
|
||||||
|
namespace LV::Client::AssetsHeaderCodec {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct ParsedModelHeader {
|
||||||
|
std::vector<ResourceId> ModelDeps;
|
||||||
|
std::vector<std::vector<uint8_t>> TexturePipelines;
|
||||||
|
std::vector<ResourceId> TextureDeps;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<std::vector<ResourceId>> parseNodestateHeaderBytes(const std::vector<uint8_t>& header) {
|
||||||
|
if(header.empty() || header.size() % sizeof(ResourceId) != 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const size_t count = header.size() / sizeof(ResourceId);
|
||||||
|
std::vector<ResourceId> deps;
|
||||||
|
deps.resize(count);
|
||||||
|
for(size_t i = 0; i < count; ++i) {
|
||||||
|
ResourceId raw = 0;
|
||||||
|
std::memcpy(&raw, header.data() + i * sizeof(ResourceId), sizeof(ResourceId));
|
||||||
|
deps[i] = raw;
|
||||||
|
}
|
||||||
|
return deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PipelineRemapResult {
|
||||||
|
bool Ok = true;
|
||||||
|
std::string Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
PipelineRemapResult remapTexturePipelineIds(std::vector<uint8_t>& code,
|
||||||
|
const std::function<uint32_t(uint32_t)>& mapId)
|
||||||
|
{
|
||||||
|
struct Range {
|
||||||
|
size_t Start = 0;
|
||||||
|
size_t End = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SrcKind : uint8_t { TexId = 0, Sub = 1 };
|
||||||
|
enum class Op : uint8_t {
|
||||||
|
End = 0,
|
||||||
|
Base_Tex = 1,
|
||||||
|
Base_Fill = 2,
|
||||||
|
Base_Anim = 3,
|
||||||
|
Resize = 10,
|
||||||
|
Transform = 11,
|
||||||
|
Opacity = 12,
|
||||||
|
NoAlpha = 13,
|
||||||
|
MakeAlpha = 14,
|
||||||
|
Invert = 15,
|
||||||
|
Brighten = 16,
|
||||||
|
Contrast = 17,
|
||||||
|
Multiply = 18,
|
||||||
|
Screen = 19,
|
||||||
|
Colorize = 20,
|
||||||
|
Anim = 21,
|
||||||
|
Overlay = 30,
|
||||||
|
Mask = 31,
|
||||||
|
LowPart = 32,
|
||||||
|
Combine = 40
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SrcMeta {
|
||||||
|
SrcKind Kind = SrcKind::TexId;
|
||||||
|
uint32_t TexId = 0;
|
||||||
|
uint32_t Off = 0;
|
||||||
|
uint32_t Len = 0;
|
||||||
|
size_t TexIdOffset = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t size = code.size();
|
||||||
|
std::vector<Range> visited;
|
||||||
|
|
||||||
|
auto read8 = [&](size_t& ip, uint8_t& out)->bool{
|
||||||
|
if(ip >= size)
|
||||||
|
return false;
|
||||||
|
out = code[ip++];
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto read16 = [&](size_t& ip, uint16_t& out)->bool{
|
||||||
|
if(ip + 1 >= size)
|
||||||
|
return false;
|
||||||
|
out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8);
|
||||||
|
ip += 2;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto read24 = [&](size_t& ip, uint32_t& out)->bool{
|
||||||
|
if(ip + 2 >= size)
|
||||||
|
return false;
|
||||||
|
out = uint32_t(code[ip])
|
||||||
|
| (uint32_t(code[ip + 1]) << 8)
|
||||||
|
| (uint32_t(code[ip + 2]) << 16);
|
||||||
|
ip += 3;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto read32 = [&](size_t& ip, uint32_t& out)->bool{
|
||||||
|
if(ip + 3 >= size)
|
||||||
|
return false;
|
||||||
|
out = uint32_t(code[ip])
|
||||||
|
| (uint32_t(code[ip + 1]) << 8)
|
||||||
|
| (uint32_t(code[ip + 2]) << 16)
|
||||||
|
| (uint32_t(code[ip + 3]) << 24);
|
||||||
|
ip += 4;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{
|
||||||
|
uint8_t kind = 0;
|
||||||
|
if(!read8(ip, kind))
|
||||||
|
return false;
|
||||||
|
out.Kind = static_cast<SrcKind>(kind);
|
||||||
|
if(out.Kind == SrcKind::TexId) {
|
||||||
|
out.TexIdOffset = ip;
|
||||||
|
return read24(ip, out.TexId);
|
||||||
|
}
|
||||||
|
if(out.Kind == SrcKind::Sub) {
|
||||||
|
return read24(ip, out.Off) && read24(ip, out.Len);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto patchTexId = [&](const SrcMeta& src)->PipelineRemapResult{
|
||||||
|
if(src.Kind != SrcKind::TexId)
|
||||||
|
return {};
|
||||||
|
uint32_t newId = mapId(src.TexId);
|
||||||
|
if(newId >= (1u << 24))
|
||||||
|
return {false, "TexId exceeds u24 range"};
|
||||||
|
if(src.TexIdOffset + 2 >= code.size())
|
||||||
|
return {false, "TexId patch outside pipeline"};
|
||||||
|
code[src.TexIdOffset + 0] = uint8_t(newId & 0xFFu);
|
||||||
|
code[src.TexIdOffset + 1] = uint8_t((newId >> 8) & 0xFFu);
|
||||||
|
code[src.TexIdOffset + 2] = uint8_t((newId >> 16) & 0xFFu);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::function<bool(size_t, size_t)> scan;
|
||||||
|
scan = [&](size_t start, size_t end) -> bool {
|
||||||
|
if(start >= end || end > size)
|
||||||
|
return true;
|
||||||
|
for(const auto& range : visited) {
|
||||||
|
if(range.Start == start && range.End == end)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
visited.push_back(Range{start, end});
|
||||||
|
|
||||||
|
size_t ip = start;
|
||||||
|
while(ip < end) {
|
||||||
|
uint8_t opByte = 0;
|
||||||
|
if(!read8(ip, opByte))
|
||||||
|
return false;
|
||||||
|
Op op = static_cast<Op>(opByte);
|
||||||
|
switch(op) {
|
||||||
|
case Op::End:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Op::Base_Tex: {
|
||||||
|
SrcMeta src{};
|
||||||
|
if(!readSrc(ip, src))
|
||||||
|
return false;
|
||||||
|
PipelineRemapResult r = patchTexId(src);
|
||||||
|
if(!r.Ok)
|
||||||
|
return false;
|
||||||
|
if(src.Kind == SrcKind::Sub) {
|
||||||
|
size_t subStart = src.Off;
|
||||||
|
size_t subEnd = subStart + src.Len;
|
||||||
|
if(!scan(subStart, subEnd))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Base_Fill: {
|
||||||
|
uint16_t tmp16 = 0;
|
||||||
|
uint32_t tmp32 = 0;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
if(!read32(ip, tmp32)) return false;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Base_Anim: {
|
||||||
|
SrcMeta src{};
|
||||||
|
if(!readSrc(ip, src)) return false;
|
||||||
|
PipelineRemapResult r = patchTexId(src);
|
||||||
|
if(!r.Ok) return false;
|
||||||
|
uint16_t frameW = 0;
|
||||||
|
uint16_t frameH = 0;
|
||||||
|
uint16_t frameCount = 0;
|
||||||
|
uint16_t fpsQ = 0;
|
||||||
|
uint8_t flags = 0;
|
||||||
|
if(!read16(ip, frameW)) return false;
|
||||||
|
if(!read16(ip, frameH)) return false;
|
||||||
|
if(!read16(ip, frameCount)) return false;
|
||||||
|
if(!read16(ip, fpsQ)) return false;
|
||||||
|
if(!read8(ip, flags)) return false;
|
||||||
|
(void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags;
|
||||||
|
if(src.Kind == SrcKind::Sub) {
|
||||||
|
size_t subStart = src.Off;
|
||||||
|
size_t subEnd = subStart + src.Len;
|
||||||
|
if(!scan(subStart, subEnd)) return false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Resize: {
|
||||||
|
uint16_t tmp16 = 0;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Transform:
|
||||||
|
case Op::Opacity:
|
||||||
|
case Op::Invert:
|
||||||
|
if(!read8(ip, opByte)) return false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Op::NoAlpha:
|
||||||
|
case Op::Brighten:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Op::MakeAlpha:
|
||||||
|
if(ip + 2 >= size) return false;
|
||||||
|
ip += 3;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Op::Contrast:
|
||||||
|
if(ip + 1 >= size) return false;
|
||||||
|
ip += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Op::Multiply:
|
||||||
|
case Op::Screen: {
|
||||||
|
uint32_t tmp32 = 0;
|
||||||
|
if(!read32(ip, tmp32)) return false;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Colorize: {
|
||||||
|
uint32_t tmp32 = 0;
|
||||||
|
if(!read32(ip, tmp32)) return false;
|
||||||
|
if(!read8(ip, opByte)) return false;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Anim: {
|
||||||
|
uint16_t frameW = 0;
|
||||||
|
uint16_t frameH = 0;
|
||||||
|
uint16_t frameCount = 0;
|
||||||
|
uint16_t fpsQ = 0;
|
||||||
|
uint8_t flags = 0;
|
||||||
|
if(!read16(ip, frameW)) return false;
|
||||||
|
if(!read16(ip, frameH)) return false;
|
||||||
|
if(!read16(ip, frameCount)) return false;
|
||||||
|
if(!read16(ip, fpsQ)) return false;
|
||||||
|
if(!read8(ip, flags)) return false;
|
||||||
|
(void)frameW; (void)frameH; (void)frameCount; (void)fpsQ; (void)flags;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Overlay:
|
||||||
|
case Op::Mask: {
|
||||||
|
SrcMeta src{};
|
||||||
|
if(!readSrc(ip, src)) return false;
|
||||||
|
PipelineRemapResult r = patchTexId(src);
|
||||||
|
if(!r.Ok) return false;
|
||||||
|
if(src.Kind == SrcKind::Sub) {
|
||||||
|
size_t subStart = src.Off;
|
||||||
|
size_t subEnd = subStart + src.Len;
|
||||||
|
if(!scan(subStart, subEnd)) return false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::LowPart: {
|
||||||
|
if(!read8(ip, opByte)) return false;
|
||||||
|
SrcMeta src{};
|
||||||
|
if(!readSrc(ip, src)) return false;
|
||||||
|
PipelineRemapResult r = patchTexId(src);
|
||||||
|
if(!r.Ok) return false;
|
||||||
|
if(src.Kind == SrcKind::Sub) {
|
||||||
|
size_t subStart = src.Off;
|
||||||
|
size_t subEnd = subStart + src.Len;
|
||||||
|
if(!scan(subStart, subEnd)) return false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Op::Combine: {
|
||||||
|
uint16_t w = 0, h = 0, n = 0;
|
||||||
|
if(!read16(ip, w)) return false;
|
||||||
|
if(!read16(ip, h)) return false;
|
||||||
|
if(!read16(ip, n)) return false;
|
||||||
|
for(uint16_t i = 0; i < n; ++i) {
|
||||||
|
uint16_t tmp16 = 0;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
if(!read16(ip, tmp16)) return false;
|
||||||
|
SrcMeta src{};
|
||||||
|
if(!readSrc(ip, src)) return false;
|
||||||
|
PipelineRemapResult r = patchTexId(src);
|
||||||
|
if(!r.Ok) return false;
|
||||||
|
if(src.Kind == SrcKind::Sub) {
|
||||||
|
size_t subStart = src.Off;
|
||||||
|
size_t subEnd = subStart + src.Len;
|
||||||
|
if(!scan(subStart, subEnd)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(void)w; (void)h;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!scan(0, size))
|
||||||
|
return {false, "Invalid texture pipeline bytecode"};
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> collectTexturePipelineIds(const std::vector<uint8_t>& code) {
|
||||||
|
std::vector<uint32_t> out;
|
||||||
|
std::unordered_set<uint32_t> seen;
|
||||||
|
|
||||||
|
auto addId = [&](uint32_t id) {
|
||||||
|
if(seen.insert(id).second)
|
||||||
|
out.push_back(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> copy = code;
|
||||||
|
auto result = remapTexturePipelineIds(copy, [&](uint32_t id) {
|
||||||
|
addId(id);
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!result.Ok)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ParsedModelHeader> parseModelHeaderBytes(const std::vector<uint8_t>& header) {
|
||||||
|
if(header.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
ParsedModelHeader result;
|
||||||
|
try {
|
||||||
|
TOS::ByteBuffer buffer(header.size(), header.data());
|
||||||
|
auto reader = buffer.reader();
|
||||||
|
|
||||||
|
uint16_t modelCount = reader.readUInt16();
|
||||||
|
result.ModelDeps.reserve(modelCount);
|
||||||
|
for(uint16_t i = 0; i < modelCount; ++i)
|
||||||
|
result.ModelDeps.push_back(reader.readUInt32());
|
||||||
|
|
||||||
|
uint16_t texCount = reader.readUInt16();
|
||||||
|
result.TexturePipelines.reserve(texCount);
|
||||||
|
for(uint16_t i = 0; i < texCount; ++i) {
|
||||||
|
uint32_t size32 = reader.readUInt32();
|
||||||
|
TOS::ByteBuffer pipe;
|
||||||
|
reader.readBuffer(pipe);
|
||||||
|
if(pipe.size() != size32)
|
||||||
|
return std::nullopt;
|
||||||
|
result.TexturePipelines.emplace_back(pipe.begin(), pipe.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<ResourceId> seen;
|
||||||
|
for(const auto& pipe : result.TexturePipelines) {
|
||||||
|
for(uint32_t id : collectTexturePipelineIds(pipe)) {
|
||||||
|
if(seen.insert(id).second)
|
||||||
|
result.TextureDeps.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(const std::exception&) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<ParsedHeader> parseHeader(EnumAssets type, const std::vector<uint8_t>& header) {
|
||||||
|
if(header.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
ParsedHeader result;
|
||||||
|
result.Type = type;
|
||||||
|
|
||||||
|
if(type == EnumAssets::Nodestate) {
|
||||||
|
auto deps = parseNodestateHeaderBytes(header);
|
||||||
|
if(!deps)
|
||||||
|
return std::nullopt;
|
||||||
|
result.ModelDeps = std::move(*deps);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == EnumAssets::Model) {
|
||||||
|
auto parsed = parseModelHeaderBytes(header);
|
||||||
|
if(!parsed)
|
||||||
|
return std::nullopt;
|
||||||
|
result.ModelDeps = std::move(parsed->ModelDeps);
|
||||||
|
result.TexturePipelines = std::move(parsed->TexturePipelines);
|
||||||
|
result.TextureDeps = std::move(parsed->TextureDeps);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> rebindHeader(EnumAssets type, const std::vector<uint8_t>& header,
|
||||||
|
const MapIdFn& mapModelId, const MapIdFn& mapTextureId, const WarnFn& warn)
|
||||||
|
{
|
||||||
|
if(header.empty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if(type == EnumAssets::Nodestate) {
|
||||||
|
if(header.size() % sizeof(ResourceId) != 0)
|
||||||
|
return header;
|
||||||
|
std::vector<uint8_t> out(header.size());
|
||||||
|
const size_t count = header.size() / sizeof(ResourceId);
|
||||||
|
for(size_t i = 0; i < count; ++i) {
|
||||||
|
ResourceId raw = 0;
|
||||||
|
std::memcpy(&raw, header.data() + i * sizeof(ResourceId), sizeof(ResourceId));
|
||||||
|
ResourceId mapped = mapModelId(raw);
|
||||||
|
std::memcpy(out.data() + i * sizeof(ResourceId), &mapped, sizeof(ResourceId));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == EnumAssets::Model) {
|
||||||
|
try {
|
||||||
|
TOS::ByteBuffer buffer(header.size(), header.data());
|
||||||
|
auto reader = buffer.reader();
|
||||||
|
|
||||||
|
uint16_t modelCount = reader.readUInt16();
|
||||||
|
std::vector<ResourceId> models;
|
||||||
|
models.reserve(modelCount);
|
||||||
|
for(uint16_t i = 0; i < modelCount; ++i) {
|
||||||
|
ResourceId id = reader.readUInt32();
|
||||||
|
models.push_back(mapModelId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t texCount = reader.readUInt16();
|
||||||
|
std::vector<std::vector<uint8_t>> pipelines;
|
||||||
|
pipelines.reserve(texCount);
|
||||||
|
for(uint16_t i = 0; i < texCount; ++i) {
|
||||||
|
uint32_t size32 = reader.readUInt32();
|
||||||
|
TOS::ByteBuffer pipe;
|
||||||
|
reader.readBuffer(pipe);
|
||||||
|
if(pipe.size() != size32) {
|
||||||
|
warn("Pipeline size mismatch");
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> code(pipe.begin(), pipe.end());
|
||||||
|
auto result = remapTexturePipelineIds(code, [&](uint32_t id) {
|
||||||
|
return mapTextureId(static_cast<ResourceId>(id));
|
||||||
|
});
|
||||||
|
if(!result.Ok) {
|
||||||
|
warn(result.Error);
|
||||||
|
}
|
||||||
|
pipelines.emplace_back(std::move(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
TOS::ByteBuffer::Writer wr;
|
||||||
|
wr << uint16_t(models.size());
|
||||||
|
for(ResourceId id : models)
|
||||||
|
wr << id;
|
||||||
|
wr << uint16_t(pipelines.size());
|
||||||
|
for(const auto& pipe : pipelines) {
|
||||||
|
wr << uint32_t(pipe.size());
|
||||||
|
TOS::ByteBuffer pipeBuff(pipe.begin(), pipe.end());
|
||||||
|
wr << pipeBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
TOS::ByteBuffer out = wr.complite();
|
||||||
|
return std::vector<uint8_t>(out.begin(), out.end());
|
||||||
|
} catch(const std::exception&) {
|
||||||
|
warn("Failed to rebind model header");
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace LV::Client::AssetsHeaderCodec
|
||||||
27
Src/Client/AssetsHeaderCodec.hpp
Normal file
27
Src/Client/AssetsHeaderCodec.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "Common/Abstract.hpp"
|
||||||
|
|
||||||
|
namespace LV::Client::AssetsHeaderCodec {
|
||||||
|
|
||||||
|
struct ParsedHeader {
|
||||||
|
EnumAssets Type{};
|
||||||
|
std::vector<ResourceId> ModelDeps;
|
||||||
|
std::vector<ResourceId> TextureDeps;
|
||||||
|
std::vector<std::vector<uint8_t>> TexturePipelines;
|
||||||
|
};
|
||||||
|
|
||||||
|
using MapIdFn = std::function<ResourceId(ResourceId)>;
|
||||||
|
using WarnFn = std::function<void(const std::string&)>;
|
||||||
|
|
||||||
|
std::optional<ParsedHeader> parseHeader(EnumAssets type, const std::vector<uint8_t>& header);
|
||||||
|
|
||||||
|
std::vector<uint8_t> rebindHeader(EnumAssets type, const std::vector<uint8_t>& header,
|
||||||
|
const MapIdFn& mapModelId, const MapIdFn& mapTextureId, const WarnFn& warn);
|
||||||
|
|
||||||
|
} // namespace LV::Client::AssetsHeaderCodec
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "Client/AssetsCacheManager.hpp"
|
#include "Client/AssetsCacheManager.hpp"
|
||||||
|
#include "Client/AssetsHeaderCodec.hpp"
|
||||||
#include "Common/Abstract.hpp"
|
#include "Common/Abstract.hpp"
|
||||||
#include "TOSLib.hpp"
|
#include "TOSLib.hpp"
|
||||||
|
|
||||||
@@ -25,88 +26,131 @@ public:
|
|||||||
using AssetType = EnumAssets;
|
using AssetType = EnumAssets;
|
||||||
using AssetId = ResourceId;
|
using AssetId = ResourceId;
|
||||||
|
|
||||||
|
// Ключ запроса ресурса (идентификация + хеш для поиска источника).
|
||||||
struct ResourceKey {
|
struct ResourceKey {
|
||||||
|
// Хеш ресурса, используемый для поиска в источниках и кэше.
|
||||||
Hash_t Hash{};
|
Hash_t Hash{};
|
||||||
|
// Тип ресурса (модель, текстура и т.д.).
|
||||||
AssetType Type{};
|
AssetType Type{};
|
||||||
|
// Домен ресурса.
|
||||||
std::string Domain;
|
std::string Domain;
|
||||||
|
// Ключ ресурса внутри домена.
|
||||||
std::string Key;
|
std::string Key;
|
||||||
|
// Идентификатор ресурса на стороне клиента/локальный.
|
||||||
AssetId Id = 0;
|
AssetId Id = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Информация о биндинге серверного ресурса на локальный id.
|
||||||
struct BindInfo {
|
struct BindInfo {
|
||||||
|
// Тип ресурса.
|
||||||
AssetType Type{};
|
AssetType Type{};
|
||||||
|
// Локальный идентификатор.
|
||||||
AssetId LocalId = 0;
|
AssetId LocalId = 0;
|
||||||
|
// Домен ресурса.
|
||||||
std::string Domain;
|
std::string Domain;
|
||||||
|
// Ключ ресурса.
|
||||||
std::string Key;
|
std::string Key;
|
||||||
|
// Хеш ресурса.
|
||||||
Hash_t Hash{};
|
Hash_t Hash{};
|
||||||
|
// Бинарный заголовок с зависимостями.
|
||||||
std::vector<uint8_t> Header;
|
std::vector<uint8_t> Header;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Результат биндинга ресурса сервера.
|
||||||
struct BindResult {
|
struct BindResult {
|
||||||
|
// Итоговый локальный идентификатор.
|
||||||
AssetId LocalId = 0;
|
AssetId LocalId = 0;
|
||||||
|
// Признак изменения бинда (хеш/заголовок).
|
||||||
bool Changed = false;
|
bool Changed = false;
|
||||||
|
// Признак новой привязки.
|
||||||
bool NewBinding = false;
|
bool NewBinding = false;
|
||||||
|
// Идентификатор, от которого произошёл ребинд (если был).
|
||||||
std::optional<AssetId> ReboundFrom;
|
std::optional<AssetId> ReboundFrom;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Регистрация набора ресурспаков.
|
||||||
struct PackRegister {
|
struct PackRegister {
|
||||||
|
// Пути до паков (директории/архивы).
|
||||||
std::vector<fs::path> Packs;
|
std::vector<fs::path> Packs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ресурс, собранный из пака.
|
||||||
struct PackResource {
|
struct PackResource {
|
||||||
|
// Тип ресурса.
|
||||||
AssetType Type{};
|
AssetType Type{};
|
||||||
|
// Локальный идентификатор.
|
||||||
AssetId LocalId = 0;
|
AssetId LocalId = 0;
|
||||||
|
// Домен ресурса.
|
||||||
std::string Domain;
|
std::string Domain;
|
||||||
|
// Ключ ресурса.
|
||||||
std::string Key;
|
std::string Key;
|
||||||
|
// Тело ресурса.
|
||||||
Resource Res;
|
Resource Res;
|
||||||
|
// Хеш ресурса.
|
||||||
Hash_t Hash{};
|
Hash_t Hash{};
|
||||||
|
// Заголовок ресурса (например, зависимости).
|
||||||
std::u8string Header;
|
std::u8string Header;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Результат пересканирования паков.
|
||||||
struct PackReloadResult {
|
struct PackReloadResult {
|
||||||
|
// Добавленные/изменённые ресурсы по типам.
|
||||||
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> ChangeOrAdd;
|
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> ChangeOrAdd;
|
||||||
|
// Потерянные ресурсы по типам.
|
||||||
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> Lost;
|
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> Lost;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ParsedHeader {
|
using ParsedHeader = AssetsHeaderCodec::ParsedHeader;
|
||||||
AssetType Type{};
|
|
||||||
std::vector<AssetId> ModelDeps;
|
|
||||||
std::vector<AssetId> TextureDeps;
|
|
||||||
std::vector<std::vector<uint8_t>> TexturePipelines;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Фабрика с настройкой лимитов кэша.
|
||||||
static Ptr Create(asio::io_context& ioc, const fs::path& cachePath,
|
static Ptr Create(asio::io_context& ioc, const fs::path& cachePath,
|
||||||
size_t maxCacheDirectorySize = 8 * 1024 * 1024 * 1024ULL,
|
size_t maxCacheDirectorySize = 8 * 1024 * 1024 * 1024ULL,
|
||||||
size_t maxLifeTime = 7 * 24 * 60 * 60) {
|
size_t maxLifeTime = 7 * 24 * 60 * 60) {
|
||||||
return Ptr(new AssetsManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime));
|
return Ptr(new AssetsManager(ioc, cachePath, maxCacheDirectorySize, maxLifeTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пересканировать ресурспаки и вернуть изменившиеся/утраченные ресурсы.
|
||||||
PackReloadResult reloadPacks(const PackRegister& reg);
|
PackReloadResult reloadPacks(const PackRegister& reg);
|
||||||
|
|
||||||
|
// Связать серверный ресурс с локальным id и записать метаданные.
|
||||||
BindResult bindServerResource(AssetType type, AssetId serverId, std::string domain, std::string key,
|
BindResult bindServerResource(AssetType type, AssetId serverId, std::string domain, std::string key,
|
||||||
const Hash_t& hash, std::vector<uint8_t> header);
|
const Hash_t& hash, std::vector<uint8_t> header);
|
||||||
|
// Отвязать серверный id и вернуть актуальный локальный id (если был).
|
||||||
std::optional<AssetId> unbindServerResource(AssetType type, AssetId serverId);
|
std::optional<AssetId> unbindServerResource(AssetType type, AssetId serverId);
|
||||||
|
// Сбросить все серверные бинды.
|
||||||
void clearServerBindings();
|
void clearServerBindings();
|
||||||
|
|
||||||
|
// Получить данные бинда по локальному id.
|
||||||
const BindInfo* getBind(AssetType type, AssetId localId) const;
|
const BindInfo* getBind(AssetType type, AssetId localId) const;
|
||||||
|
|
||||||
|
// Перебиндить хедер, заменив id зависимостей.
|
||||||
std::vector<uint8_t> rebindHeader(AssetType type, const std::vector<uint8_t>& header, bool serverIds = true);
|
std::vector<uint8_t> rebindHeader(AssetType type, const std::vector<uint8_t>& header, bool serverIds = true);
|
||||||
|
// Распарсить хедер ресурса.
|
||||||
static std::optional<ParsedHeader> parseHeader(AssetType type, const std::vector<uint8_t>& header);
|
static std::optional<ParsedHeader> parseHeader(AssetType type, const std::vector<uint8_t>& header);
|
||||||
|
|
||||||
void pushResources(std::vector<Resource> resources) {
|
// Протолкнуть новые ресурсы в память и кэш.
|
||||||
Cache->pushResources(std::move(resources));
|
void pushResources(std::vector<Resource> resources);
|
||||||
}
|
|
||||||
|
|
||||||
|
// Поставить запросы чтения ресурсов.
|
||||||
void pushReads(std::vector<ResourceKey> reads);
|
void pushReads(std::vector<ResourceKey> reads);
|
||||||
|
// Получить готовые результаты чтения.
|
||||||
std::vector<std::pair<ResourceKey, std::optional<Resource>>> pullReads();
|
std::vector<std::pair<ResourceKey, std::optional<Resource>>> pullReads();
|
||||||
|
// Продвинуть асинхронные источники (кэш).
|
||||||
|
void tickSources();
|
||||||
|
|
||||||
|
// Получить или создать локальный id по домену/ключу.
|
||||||
AssetId getOrCreateLocalId(AssetType type, std::string_view domain, std::string_view key);
|
AssetId getOrCreateLocalId(AssetType type, std::string_view domain, std::string_view key);
|
||||||
|
// Получить локальный id по серверному id (если есть).
|
||||||
std::optional<AssetId> getLocalIdFromServer(AssetType type, AssetId serverId) const;
|
std::optional<AssetId> getLocalIdFromServer(AssetType type, AssetId serverId) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Связка домен/ключ для локального id.
|
||||||
struct DomainKey {
|
struct DomainKey {
|
||||||
|
// Домен ресурса.
|
||||||
std::string Domain;
|
std::string Domain;
|
||||||
|
// Ключ ресурса.
|
||||||
std::string Key;
|
std::string Key;
|
||||||
|
// Признак валидности записи.
|
||||||
bool Known = false;
|
bool Known = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,28 +166,127 @@ private:
|
|||||||
detail::TSVHash,
|
detail::TSVHash,
|
||||||
detail::TSVEq>;
|
detail::TSVEq>;
|
||||||
|
|
||||||
|
struct PerType {
|
||||||
|
// Таблица домен/ключ -> локальный id.
|
||||||
|
IdTable DKToLocal;
|
||||||
|
// Таблица локальный id -> домен/ключ.
|
||||||
|
std::vector<DomainKey> LocalToDK;
|
||||||
|
// Union-Find родительские ссылки для ребиндов.
|
||||||
|
std::vector<AssetId> LocalParent;
|
||||||
|
// Таблица серверный id -> локальный id.
|
||||||
|
std::vector<AssetId> ServerToLocal;
|
||||||
|
// Бинды с сервером по локальному id.
|
||||||
|
std::vector<std::optional<BindInfo>> BindInfos;
|
||||||
|
// Ресурсы, собранные из паков.
|
||||||
|
PackTable PackResources;
|
||||||
|
// Следующий локальный id.
|
||||||
|
AssetId NextLocalId = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SourceStatus {
|
||||||
|
Hit,
|
||||||
|
Miss,
|
||||||
|
Pending
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SourceResult {
|
||||||
|
// Статус ответа источника.
|
||||||
|
SourceStatus Status = SourceStatus::Miss;
|
||||||
|
// Значение ресурса, если найден.
|
||||||
|
std::optional<Resource> Value;
|
||||||
|
// Индекс источника.
|
||||||
|
size_t SourceIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SourceReady {
|
||||||
|
// Хеш готового ресурса.
|
||||||
|
Hash_t Hash{};
|
||||||
|
// Значение ресурса, если найден.
|
||||||
|
std::optional<Resource> Value;
|
||||||
|
// Индекс источника.
|
||||||
|
size_t SourceIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IResourceSource {
|
||||||
|
public:
|
||||||
|
virtual ~IResourceSource() = default;
|
||||||
|
// Попытка получить ресурс синхронно.
|
||||||
|
virtual SourceResult tryGet(const ResourceKey& key) = 0;
|
||||||
|
// Забрать готовые результаты асинхронных запросов.
|
||||||
|
virtual void collectReady(std::vector<SourceReady>& out) = 0;
|
||||||
|
// Признак асинхронности источника.
|
||||||
|
virtual bool isAsync() const = 0;
|
||||||
|
// Запустить асинхронные запросы по хешам.
|
||||||
|
virtual void startPending(std::vector<Hash_t> hashes) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SourceEntry {
|
||||||
|
// Экземпляр источника.
|
||||||
|
std::unique_ptr<IResourceSource> Source;
|
||||||
|
// Поколение для инвалидирования кэша.
|
||||||
|
size_t Generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SourceCacheEntry {
|
||||||
|
// Индекс источника, где был найден хеш.
|
||||||
|
size_t SourceIndex = 0;
|
||||||
|
// Поколение источника на момент кэширования.
|
||||||
|
size_t Generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Конструктор с зависимостью от io_context и кэш-пути.
|
||||||
AssetsManager(asio::io_context& ioc, const fs::path& cachePath,
|
AssetsManager(asio::io_context& ioc, const fs::path& cachePath,
|
||||||
size_t maxCacheDirectorySize, size_t maxLifeTime);
|
size_t maxCacheDirectorySize, size_t maxLifeTime);
|
||||||
|
|
||||||
|
// Инициализация списка источников.
|
||||||
|
void initSources();
|
||||||
|
// Забрать готовые результаты из источников.
|
||||||
|
void collectReadyFromSources();
|
||||||
|
// Запросить ресурс в источниках, с учётом кэша.
|
||||||
|
SourceResult querySources(const ResourceKey& key);
|
||||||
|
// Запомнить успешный источник для хеша.
|
||||||
|
void registerSourceHit(const Hash_t& hash, size_t sourceIndex);
|
||||||
|
// Инвалидировать кэш по конкретному источнику.
|
||||||
|
void invalidateSourceCache(size_t sourceIndex);
|
||||||
|
// Инвалидировать весь кэш источников.
|
||||||
|
void invalidateAllSourceCache();
|
||||||
|
|
||||||
|
// Выделить новый локальный id.
|
||||||
AssetId allocateLocalId(AssetType type);
|
AssetId allocateLocalId(AssetType type);
|
||||||
|
// Получить корневой локальный id с компрессией пути.
|
||||||
AssetId resolveLocalIdMutable(AssetType type, AssetId localId);
|
AssetId resolveLocalIdMutable(AssetType type, AssetId localId);
|
||||||
|
// Получить корневой локальный id без мутаций.
|
||||||
AssetId resolveLocalId(AssetType type, AssetId localId) const;
|
AssetId resolveLocalId(AssetType type, AssetId localId) const;
|
||||||
|
// Объединить два локальных id в один.
|
||||||
void unionLocalIds(AssetType type, AssetId fromId, AssetId toId, std::optional<AssetId>* reboundFrom);
|
void unionLocalIds(AssetType type, AssetId fromId, AssetId toId, std::optional<AssetId>* reboundFrom);
|
||||||
|
|
||||||
|
// Найти ресурс в паке по домену/ключу.
|
||||||
std::optional<PackResource> findPackResource(AssetType type, std::string_view domain, std::string_view key) const;
|
std::optional<PackResource> findPackResource(AssetType type, std::string_view domain, std::string_view key) const;
|
||||||
|
|
||||||
|
// Логгер подсистемы.
|
||||||
Logger LOG = "Client>AssetsManager";
|
Logger LOG = "Client>AssetsManager";
|
||||||
|
// Менеджер файлового кэша.
|
||||||
AssetsCacheManager::Ptr Cache;
|
AssetsCacheManager::Ptr Cache;
|
||||||
|
|
||||||
std::array<IdTable, static_cast<size_t>(AssetType::MAX_ENUM)> DKToLocal;
|
// Таблицы данных по каждому типу ресурсов.
|
||||||
std::array<std::vector<DomainKey>, static_cast<size_t>(AssetType::MAX_ENUM)> LocalToDK;
|
std::array<PerType, static_cast<size_t>(AssetType::MAX_ENUM)> Types;
|
||||||
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> LocalParent;
|
|
||||||
std::array<std::vector<AssetId>, static_cast<size_t>(AssetType::MAX_ENUM)> ServerToLocal;
|
|
||||||
std::array<std::vector<std::optional<BindInfo>>, static_cast<size_t>(AssetType::MAX_ENUM)> BindInfos;
|
|
||||||
std::array<PackTable, static_cast<size_t>(AssetType::MAX_ENUM)> PackResources;
|
|
||||||
std::array<AssetId, static_cast<size_t>(AssetType::MAX_ENUM)> NextLocalId{};
|
|
||||||
|
|
||||||
|
// Список источников ресурсов.
|
||||||
|
std::vector<SourceEntry> Sources;
|
||||||
|
// Кэш попаданий по хешу.
|
||||||
|
std::unordered_map<Hash_t, SourceCacheEntry> SourceCacheByHash;
|
||||||
|
// Индекс источника паков.
|
||||||
|
size_t PackSourceIndex = 0;
|
||||||
|
// Индекс памяти (RAM) как источника.
|
||||||
|
size_t MemorySourceIndex = 0;
|
||||||
|
// Индекс файлового кэша.
|
||||||
|
size_t CacheSourceIndex = 0;
|
||||||
|
|
||||||
|
// Ресурсы в памяти по хешу.
|
||||||
|
std::unordered_map<Hash_t, Resource> MemoryResourcesByHash;
|
||||||
|
// Ожидающие запросы, сгруппированные по хешу.
|
||||||
std::unordered_map<Hash_t, std::vector<ResourceKey>> PendingReadsByHash;
|
std::unordered_map<Hash_t, std::vector<ResourceKey>> PendingReadsByHash;
|
||||||
|
// Готовые ответы на чтение.
|
||||||
std::vector<std::pair<ResourceKey, std::optional<Resource>>> ReadyReads;
|
std::vector<std::pair<ResourceKey, std::optional<Resource>>> ReadyReads;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
|
|||||||
if (assetType == AssetType::Texture && file.extension() == ".meta")
|
if (assetType == AssetType::Texture && file.extension() == ".meta")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::string key = fs::relative(file, assetPath).string();
|
std::string key = fs::relative(file, assetPath).generic_string();
|
||||||
if (firstStage.contains(key))
|
if (firstStage.contains(key))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ AssetsPreloader::Out_reloadResources AssetsPreloader::_reloadResources(const Ass
|
|||||||
fs::path pKeyPath = fs::path(pKeyRaw);
|
fs::path pKeyPath = fs::path(pKeyRaw);
|
||||||
if(pKeyPath.extension().empty())
|
if(pKeyPath.extension().empty())
|
||||||
pKeyPath += ".json";
|
pKeyPath += ".json";
|
||||||
std::string pKey = pKeyPath.string();
|
std::string pKey = pKeyPath.generic_string();
|
||||||
|
|
||||||
std::optional<js::object> parent = loadModelProfile(pDomain, pKey, visiting);
|
std::optional<js::object> parent = loadModelProfile(pDomain, pKey, visiting);
|
||||||
if(parent) {
|
if(parent) {
|
||||||
|
|||||||
66
docs/assets_manager.md
Normal file
66
docs/assets_manager.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# AssetsManager
|
||||||
|
|
||||||
|
Документ описывает реализацию `AssetsManager` на стороне клиента.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
`AssetsManager` объединяет в одном объекте:
|
||||||
|
- таблицы привязок ресурсов (domain/key -> localId, serverId -> localId);
|
||||||
|
- загрузку и хранение ресурсов из ресурспаков;
|
||||||
|
- систему источников данных (packs/memory/cache) с асинхронной выдачей;
|
||||||
|
- перестройку заголовков ресурсов под локальные идентификаторы.
|
||||||
|
|
||||||
|
## Основные структуры данных
|
||||||
|
|
||||||
|
- `PerType` — набор таблиц на каждый тип ресурса:
|
||||||
|
- `DKToLocal` и `LocalToDK` — двунаправленное отображение domain/key <-> localId.
|
||||||
|
- `LocalParent` — union-find для ребиндинга локальных id.
|
||||||
|
- `ServerToLocal` и `BindInfos` — привязки серверных id и их метаданные.
|
||||||
|
- `PackResources` — набор ресурсов, собранных из паков.
|
||||||
|
- `Sources` — список источников ресурсов (pack, memory, cache).
|
||||||
|
- `SourceCacheByHash` — кэш успешного источника для хеша.
|
||||||
|
- `PendingReadsByHash` и `ReadyReads` — очередь ожидания и готовые ответы.
|
||||||
|
|
||||||
|
## Источники ресурсов
|
||||||
|
|
||||||
|
Источники реализованы через интерфейс `IResourceSource`:
|
||||||
|
- pack source (sync) — ищет ресурсы в `PackResources`.
|
||||||
|
- memory source (sync) — ищет в `MemoryResourcesByHash`.
|
||||||
|
- cache source (async) — делает чтения через `AssetsCacheManager`.
|
||||||
|
|
||||||
|
Алгоритм поиска:
|
||||||
|
1) Сначала проверяется `SourceCacheByHash` (если не протух по поколению).
|
||||||
|
2) Источники опрашиваются по порядку, первый `Hit` возвращается сразу.
|
||||||
|
3) Если источник вернул `Pending`, запрос попадает в ожидание.
|
||||||
|
4) `tickSources()` опрашивает асинхронные источники и переводит ответы в `ReadyReads`.
|
||||||
|
|
||||||
|
## Привязка идентификаторов
|
||||||
|
|
||||||
|
- `getOrCreateLocalId()` создаёт локальный id для domain/key.
|
||||||
|
- `bindServerResource()` связывает serverId с localId и записывает `BindInfo`.
|
||||||
|
- `unionLocalIds()` объединяет локальные id при конфликте, используя union-find.
|
||||||
|
|
||||||
|
## Ресурспаки
|
||||||
|
|
||||||
|
`reloadPacks()` сканирует директории, собирает ресурсы в `PackResources`,
|
||||||
|
а затем возвращает список изменений и потерь по типам.
|
||||||
|
|
||||||
|
Важно: ключи ресурсов всегда хранятся с разделителем `/`.
|
||||||
|
Для нормализации пути используется `fs::path::generic_string()`.
|
||||||
|
|
||||||
|
## Заголовки
|
||||||
|
|
||||||
|
- `rebindHeader()` заменяет id зависимостей в заголовках ресурса.
|
||||||
|
- `parseHeader()` парсит заголовок без модификаций.
|
||||||
|
|
||||||
|
## Поток данных чтения
|
||||||
|
|
||||||
|
1) `pushReads()` принимает список `ResourceKey` и пытается получить ресурс.
|
||||||
|
2) `pullReads()` возвращает готовые ответы, включая промахи.
|
||||||
|
3) `pushResources()` добавляет ресурсы в память и прокидывает их в кэш.
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Класс не предназначен для внешнего многопоточного использования.
|
||||||
|
- Политика приоритета ресурсов в паке фиксированная: первый найденный ключ побеждает.
|
||||||
|
- Коллизии хешей не обрабатываются отдельно.
|
||||||
Reference in New Issue
Block a user