metaforce/Runtime/Graphics/CTextureBoo.cpp

748 lines
22 KiB
C++
Raw Normal View History

2016-02-16 05:50:41 +00:00
#include "CTexture.hpp"
#include "CSimplePool.hpp"
#include "CToken.hpp"
2016-03-04 23:04:53 +00:00
#include "Graphics/CGraphics.hpp"
2016-02-16 05:50:41 +00:00
2016-03-04 23:04:53 +00:00
namespace urde
2016-02-16 05:50:41 +00:00
{
2016-03-31 21:06:41 +00:00
static logvisor::Module Log("urde::CTextureBoo");
2016-02-16 05:50:41 +00:00
/* GX uses this upsampling technique to extract full 8-bit range */
2016-02-16 05:50:41 +00:00
static inline uint8_t Convert3To8(uint8_t v)
{
/* Swizzle bits: 00000123 -> 12312312 */
return (v << 5) | (v << 2) | (v >> 1);
}
static inline uint8_t Convert4To8(uint8_t v)
{
/* Swizzle bits: 00001234 -> 12341234 */
return (v << 4) | v;
}
static inline uint8_t Convert5To8(uint8_t v)
{
/* Swizzle bits: 00012345 -> 12345123 */
return (v << 3) | (v >> 2);
}
static inline uint8_t Convert6To8(uint8_t v)
{
/* Swizzle bits: 00123456 -> 12345612 */
return (v << 2) | (v >> 4);
}
size_t CTexture::ComputeMippedTexelCount()
{
size_t w = x4_w;
size_t h = x6_h;
size_t ret = w * h;
for (int i=x8_mips ; i>1 ; --i)
{
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
ret += w * h;
}
return ret;
}
size_t CTexture::ComputeMippedBlockCountDXT1()
{
size_t w = x4_w / 4;
size_t h = x6_h / 4;
size_t ret = w * h;
for (int i=x8_mips ; i>1 ; --i)
{
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
ret += w * h;
}
return ret;
}
struct RGBA8
{
u8 r;
u8 g;
u8 b;
u8 a;
};
void CTexture::BuildI4FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 7) / 8;
int bheight = (h + 7) / 8;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 8;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 8;
for (int y=0 ; y<8 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[4];
in.readBytesToBuf(source, 4);
for (int x=0 ; x<8 ; ++x)
{
target[x].r = Convert4To8(source[x/2] >> ((x&1)?0:4) & 0xf);
target[x].g = target[x].r;
target[x].b = target[x].r;
target[x].a = target[x].r;
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildI8FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 7) / 8;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 8;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[8];
in.readBytesToBuf(source, 8);
for (int x=0 ; x<8 ; ++x)
{
target[x].r = source[x];
target[x].g = source[x];
target[x].b = source[x];
target[x].a = source[x];
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildIA4FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 7) / 8;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 8;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[8];
in.readBytesToBuf(source, 8);
for (int x=0 ; x<8 ; ++x)
{
u8 intensity = Convert4To8(source[x] >> 4 & 0xf);
target[x].r = intensity;
target[x].g = intensity;
target[x].b = intensity;
target[x].a = Convert4To8(source[x] & 0xf);
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildIA8FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 3) / 4;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 4;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u16 source[4];
in.readBytesToBuf(source, 8);
for (int x=0 ; x<4 ; ++x)
{
u8 intensity = source[x] >> 8;
target[x].r = intensity;
target[x].g = intensity;
target[x].b = intensity;
target[x].a = source[x] & 0xff;
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
static std::vector<RGBA8> DecodePalette(int numEntries, CInputStream& in)
{
std::vector<RGBA8> ret;
ret.reserve(numEntries);
enum class EPaletteType
{
IA8,
RGB565,
RGB5A3
};
EPaletteType format = EPaletteType(in.readUint32Big());
in.readUint32Big();
switch (format)
{
case EPaletteType::IA8:
{
for (int e=0 ; e<numEntries ; ++e)
{
u8 intensity = in.readUByte();
u8 alpha = in.readUByte();
ret.push_back({intensity, intensity, intensity, alpha});
}
break;
}
case EPaletteType::RGB565:
{
for (int e=0 ; e<numEntries ; ++e)
{
u16 texel = in.readUint16Big();
ret.push_back({Convert5To8(texel >> 11 & 0x1f),
Convert6To8(texel >> 5 & 0x3f),
Convert5To8(texel & 0x1f),
0xff});
}
break;
}
case EPaletteType::RGB5A3:
{
for (int e=0 ; e<numEntries ; ++e)
{
u16 texel = in.readUint16Big();
if (texel & 0x8000)
{
ret.push_back({Convert5To8(texel >> 10 & 0x1f),
Convert5To8(texel >> 5 & 0x1f),
Convert5To8(texel & 0x1f),
0xff});
}
else
{
ret.push_back({Convert4To8(texel >> 8 & 0xf),
Convert4To8(texel >> 4 & 0xf),
Convert4To8(texel & 0xf),
Convert3To8(texel >> 12 & 0x7)});
}
}
break;
}
}
return ret;
}
void CTexture::BuildC4FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
std::vector<RGBA8> palette = DecodePalette(16, in);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 7) / 8;
int bheight = (h + 7) / 8;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 8;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 8;
for (int y=0 ; y<8 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[4];
in.readBytesToBuf(source, 4);
for (int x=0 ; x<8 ; ++x)
target[x] = palette[source[x/2] >> ((x&1)?0:4) & 0xf];
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildC8FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
std::vector<RGBA8> palette = DecodePalette(256, in);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 7) / 8;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 8;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[8];
in.readBytesToBuf(source, 8);
for (int x=0 ; x<8 ; ++x)
target[x] = palette[source[x]];
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildC14X2FromGCN(CInputStream& in)
{
}
void CTexture::BuildRGB565FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 3) / 4;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 4;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
for (int x=0 ; x<4 ; ++x)
{
u16 texel = in.readUint16Big();
target[x].r = Convert5To8(texel >> 11 & 0x1f);
target[x].g = Convert6To8(texel >> 5 & 0x3f);
target[x].b = Convert5To8(texel & 0x1f);
target[x].a = 0xff;
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildRGB5A3FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 3) / 4;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 4;
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
for (int x=0 ; x<4 ; ++x)
{
u16 texel = in.readUint16Big();
if (texel & 0x8000)
{
target[x].r = Convert5To8(texel >> 10 & 0x1f);
target[x].g = Convert5To8(texel >> 5 & 0x1f);
target[x].b = Convert5To8(texel & 0x1f);
target[x].a = 0xff;
}
else
{
target[x].r = Convert4To8(texel >> 8 & 0xf);
target[x].g = Convert4To8(texel >> 4 & 0xf);
target[x].b = Convert4To8(texel & 0xf);
target[x].a = Convert3To8(texel >> 12 & 0x7);
}
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
void CTexture::BuildRGBA8FromGCN(CInputStream& in)
{
size_t texelCount = ComputeMippedTexelCount();
std::unique_ptr<RGBA8[]> buf(new RGBA8[texelCount]);
int w = x4_w;
int h = x6_h;
RGBA8* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 3) / 4;
int bheight = (h + 3) / 4;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 4;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 4;
for (int c=0 ; c<2 ; ++c)
{
for (int y=0 ; y<4 ; ++y)
{
RGBA8* target = targetMip + (baseY + y) * w + baseX;
u8 source[8];
in.readBytesToBuf(source, 8);
for (int x=0 ; x<4 ; ++x)
{
if (c)
{
target[x].g = source[x*2];
target[x].b = source[x*2+1];
}
else
{
target[x].a = source[x*2];
target[x].r = source[x*2+1];
}
}
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
buf.get(), texelCount * 4);
return true;
});
2016-02-16 05:50:41 +00:00
}
struct DXT1Block
{
uint16_t color1;
uint16_t color2;
uint8_t lines[4];
};
void CTexture::BuildDXT1FromGCN(CInputStream& in)
{
size_t blockCount = ComputeMippedBlockCountDXT1();
std::unique_ptr<DXT1Block[]> buf(new DXT1Block[blockCount]);
int w = x4_w / 4;
int h = x6_h / 4;
DXT1Block* targetMip = buf.get();
for (int mip=0 ; mip<x8_mips ; ++mip)
{
int bwidth = (w + 1) / 2;
int bheight = (h + 1) / 2;
for (int by=0 ; by<bheight ; ++by)
{
int baseY = by * 2;
for (int bx=0 ; bx<bwidth ; ++bx)
{
int baseX = bx * 2;
for (int y=0 ; y<2 ; ++y)
{
DXT1Block* target = targetMip + (baseY + y) * w + baseX;
DXT1Block source[2];
in.readBytesToBuf(source, 16);
for (int x=0 ; x<2 ; ++x)
{
2016-03-04 23:04:53 +00:00
target[x].color1 = hecl::SBig(source[x].color1);
target[x].color2 = hecl::SBig(source[x].color2);
2016-02-16 08:52:22 +00:00
for (int i=0 ; i<4 ; ++i)
{
u8 ind[4];
u8 packed = source[x].lines[i];
ind[3] = packed & 0x3;
ind[2] = (packed >> 2) & 0x3;
ind[1] = (packed >> 4) & 0x3;
ind[0] = (packed >> 6) & 0x3;
target[x].lines[i] = ind[0] | (ind[1] << 2) | (ind[2] << 4) | (ind[3] << 6);
}
2016-02-16 05:50:41 +00:00
}
}
}
}
targetMip += w * h;
if (w > 1)
w /= 2;
if (h > 1)
h /= 2;
}
2016-03-30 19:16:01 +00:00
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::DXT1,
buf.get(), blockCount * 8);
return true;
});
2016-02-16 05:50:41 +00:00
}
2016-04-01 01:00:37 +00:00
void CTexture::BuildRGBA8(const void* data)
2016-03-31 21:06:41 +00:00
{
size_t texelCount = ComputeMippedTexelCount();
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::RGBA8,
2016-04-01 01:00:37 +00:00
data, texelCount * 4);
2016-03-31 21:06:41 +00:00
return true;
});
}
void CTexture::BuildC8(const void* data)
{
size_t texelCount = ComputeMippedTexelCount();
m_booToken = CGraphics::CommitResources([&](boo::IGraphicsDataFactory::Context& ctx) -> bool
{
uint32_t nentries = hecl::SBig(*reinterpret_cast<const uint32_t*>(data));
const u8* paletteTexels = reinterpret_cast<const u8*>(data) + 4;
const u8* texels = reinterpret_cast<const u8*>(data) + 4 + nentries * 4;
m_paletteTex = ctx.newStaticTexture(nentries, 1, 1, boo::TextureFormat::RGBA8,
paletteTexels, nentries * 4);
m_booTex = ctx.newStaticTexture(x4_w, x6_h, x8_mips, boo::TextureFormat::I8,
texels, texelCount);
return true;
});
}
2016-04-01 01:00:37 +00:00
CTexture::CTexture(std::unique_ptr<u8[]>&& in, u32 length)
2016-02-16 05:50:41 +00:00
{
2016-04-01 01:00:37 +00:00
std::unique_ptr<u8[]> owned = std::move(in);
athena::io::MemoryReader r(owned.get(), length);
x0_fmt = ETexelFormat(r.readUint32Big());
x4_w = r.readUint16Big();
x6_h = r.readUint16Big();
x8_mips = r.readUint32Big();
2016-02-16 05:50:41 +00:00
switch (x0_fmt)
{
case ETexelFormat::I4:
2016-04-01 01:00:37 +00:00
BuildI4FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::I8:
2016-04-01 01:00:37 +00:00
BuildI8FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::IA4:
2016-04-01 01:00:37 +00:00
BuildIA4FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::IA8:
2016-04-01 01:00:37 +00:00
BuildIA8FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::C4:
2016-04-01 01:00:37 +00:00
BuildC4FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::C8:
2016-04-01 01:00:37 +00:00
BuildC8FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::C14X2:
2016-04-01 01:00:37 +00:00
BuildC14X2FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::RGB565:
2016-04-01 01:00:37 +00:00
BuildRGB565FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::RGB5A3:
2016-04-01 01:00:37 +00:00
BuildRGB5A3FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::RGBA8:
2016-04-01 01:00:37 +00:00
BuildRGBA8FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
case ETexelFormat::CMPR:
2016-04-01 01:00:37 +00:00
BuildDXT1FromGCN(r);
2016-02-16 05:50:41 +00:00
break;
2016-03-31 21:06:41 +00:00
case ETexelFormat::RGBA8PC:
2016-04-01 01:00:37 +00:00
BuildRGBA8(owned.get() + 12);
2016-03-31 21:06:41 +00:00
break;
case ETexelFormat::C8PC:
BuildC8(owned.get() + 12);
break;
2016-03-31 21:06:41 +00:00
default:
Log.report(logvisor::Fatal, "invalid texture type %d for boo", int(x0_fmt));
2016-02-16 05:50:41 +00:00
}
}
2016-03-31 02:44:43 +00:00
void CTexture::Load(int slot, EClampMode clamp) const
2016-02-16 05:50:41 +00:00
{
}
2016-04-01 01:00:37 +00:00
CFactoryFnReturn FTextureFactory(const urde::SObjectTag& tag,
std::unique_ptr<u8[]>&& in, u32 len,
const urde::CVParamTransfer& vparms)
2016-02-16 05:50:41 +00:00
{
2016-04-01 01:00:37 +00:00
return TToken<CTexture>::GetIObjObjectFor(std::make_unique<CTexture>(std::move(in), len));
2016-02-16 05:50:41 +00:00
}
}