Owner-authorized source migration: 31 helper-only translation units were merged into their callers or existing inline headers; both build manifests updated. Includes the preserved source payload, Lua configuration matching wowdecomp, and exact malloca expansion without diagnostic pragmas. 449 native units compiled locally and 449 staged units compiled by the pinned server toolchain. No previously byte-matched function regressed. Compiler mapping diagnostics remain recorded as needs_cleanup, not accepted or peer-confirmed. Existing independent review records are preserved. README.md is intentionally unchanged.
661 lines
16 KiB
C++
661 lines
16 KiB
C++
#include "Contents/Configuration/ServiceOptions.h"
|
|
#include "Contents/Sound/SoundConfiguration.h"
|
|
#include "Contents/Sound/SoundMgr.h"
|
|
#include "Resource/ResourceMgr.h"
|
|
#include "Resource/Audio/WaveResource.h"
|
|
#include "Utility/Math/AffineMatrix.h"
|
|
#include "Utility/Math/ScalarConversion.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <new>
|
|
#include <process.h>
|
|
|
|
// STATIC: 0x0138BE7C
|
|
// GLOBAL: gl_24457
|
|
CSoundMgr* g_soundMgr = nullptr;
|
|
|
|
namespace {
|
|
// OFFSET: 0x00401090
|
|
// FUNCTION: fn_45443
|
|
void ShutdownMilesAtExit() {
|
|
MilesRuntime::Get().shutdown();
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x00638A90
|
|
// FUNCTION: fn_45421
|
|
CSoundMgr::CSoundMgr(SoundMgrContext& context) : m_context(&context) {
|
|
if (this->Initialize()) {
|
|
this->initialized = 1;
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x00638C40
|
|
// OFFSET: 0x00638D50
|
|
// FUNCTION: fn_45422
|
|
CSoundMgr::~CSoundMgr() {
|
|
this->Shutdown();
|
|
}
|
|
|
|
// OFFSET: 0x00638E80
|
|
// FUNCTION: fn_04747
|
|
void CSoundMgr::FadeIn() {
|
|
if (this->threadId != 0) {
|
|
PostThreadMessageA(this->threadId, WM_USER, 0, 0);
|
|
this->threadId = 0;
|
|
}
|
|
|
|
if (this->fadeThread != INVALID_HANDLE_VALUE) {
|
|
WaitForSingleObject(this->fadeThread, 30);
|
|
CloseHandle(this->fadeThread);
|
|
this->fadeThread = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
this->fadeThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, &CSoundMgr::FadeThread, reinterpret_cast<void*>(1), 0, &this->threadId));
|
|
}
|
|
|
|
// OFFSET: 0x00638EF0
|
|
// FUNCTION: fn_04748
|
|
void CSoundMgr::FadeOut() {
|
|
if (this->threadId != 0) {
|
|
PostThreadMessageA(this->threadId, WM_USER, 0, 0);
|
|
this->threadId = 0;
|
|
}
|
|
|
|
if (this->fadeThread != INVALID_HANDLE_VALUE) {
|
|
WaitForSingleObject(this->fadeThread, 30);
|
|
CloseHandle(this->fadeThread);
|
|
this->fadeThread = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
this->fadeThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, &CSoundMgr::FadeThread, nullptr, 0, &this->threadId));
|
|
}
|
|
|
|
// OFFSET: 0x00638F60
|
|
// FUNCTION: fn_04749
|
|
void CSoundMgr::StopStreams() {
|
|
this->currentName = std::string();
|
|
|
|
if (this->stream) {
|
|
MilesRuntime::Get().closeStream(this->stream);
|
|
this->stream = nullptr;
|
|
}
|
|
|
|
if (this->secondaryStream) {
|
|
MilesRuntime::Get().closeStream(this->secondaryStream);
|
|
this->secondaryStream = nullptr;
|
|
}
|
|
|
|
this->secondaryEnabled = 0;
|
|
}
|
|
|
|
// OFFSET: 0x00639070
|
|
// FUNCTION: fn_04750
|
|
bool CSoundMgr::Initialize() {
|
|
std::memset(this->samples, 0, sizeof(this->samples));
|
|
std::memset(this->samples3D, 0, sizeof(this->samples3D));
|
|
|
|
for (int i = 0; i < 64; ++i) {
|
|
this->providerNames[i] = nullptr;
|
|
this->providers[i] = 0;
|
|
}
|
|
|
|
SoundConfiguration& configuration = this->m_context->configuration;
|
|
|
|
if (configuration.mode == 0) {
|
|
return true;
|
|
}
|
|
|
|
if (configuration.numSampleType == 0) {
|
|
this->sampleCount = 48;
|
|
this->sample3DCount = 48;
|
|
} else if (configuration.numSampleType == 1) {
|
|
this->sampleCount = 32;
|
|
this->sample3DCount = 32;
|
|
} else {
|
|
this->sampleCount = 16;
|
|
this->sample3DCount = 16;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
miles.setRedistDirectory(".\\");
|
|
|
|
std::atexit(&ShutdownMilesAtExit);
|
|
miles.startup();
|
|
|
|
this->channels = configuration.mode == 1 ? 1 : 2;
|
|
|
|
if (configuration.digitalRateType == 0) {
|
|
this->rate = 44100;
|
|
} else if (configuration.digitalRateType == 1) {
|
|
this->rate = 11025;
|
|
} else {
|
|
this->rate = 8000;
|
|
}
|
|
|
|
this->bits = configuration.digitalBitsType == 0 ? 16 : 8;
|
|
miles.setPreference(15, configuration.preference15);
|
|
this->driver = miles.openDigitalDriver(this->rate, this->bits, this->channels, 1);
|
|
|
|
if (!this->driver) {
|
|
return false;
|
|
}
|
|
|
|
this->StopStreams();
|
|
|
|
if (configuration.mode == 1) {
|
|
for (int i = 0; i < this->sampleCount; ++i) {
|
|
this->samples[i] = miles.allocateSample(this->driver);
|
|
|
|
if (!this->samples[i]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (configuration.mode != 2) {
|
|
return true;
|
|
}
|
|
|
|
strcpy_s(configuration.providerName, sizeof(configuration.providerName), "Miles Fast 2D Positional Audio");
|
|
|
|
int index = 0;
|
|
|
|
while (miles.enumerate3DProviders(&this->enumerator, &this->providers[index], &this->providerNames[index])) {
|
|
if (std::strcmp(this->providerNames[index], configuration.providerName) == 0) {
|
|
this->provider = this->providers[index];
|
|
break;
|
|
}
|
|
|
|
++index;
|
|
}
|
|
|
|
if (!this->provider) {
|
|
return true;
|
|
}
|
|
|
|
if (miles.open3DProvider(this->provider) != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (miles.speakerType(this->provider) != -1) {
|
|
miles.setSpeakerType(this->provider, configuration.speakerType);
|
|
}
|
|
|
|
this->listener = miles.open3DListener(this->provider);
|
|
|
|
if (!this->listener) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < this->sample3DCount; ++i) {
|
|
this->samples3D[i] = miles.allocate3DSample(this->provider);
|
|
|
|
if (!this->samples3D[i]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// OFFSET: 0x00639350
|
|
// FUNCTION: fn_04751
|
|
bool CSoundMgr::Create(SoundMgrContext& context) {
|
|
if (g_soundMgr) {
|
|
return false;
|
|
}
|
|
|
|
g_soundMgr = new (std::nothrow) CSoundMgr(context);
|
|
|
|
if (!g_soundMgr) {
|
|
return false;
|
|
}
|
|
|
|
if (!g_soundMgr->initialized) {
|
|
delete g_soundMgr;
|
|
g_soundMgr = nullptr;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// OFFSET: 0x00639570
|
|
// FUNCTION: fn_04752
|
|
void CSoundMgr::Update() {
|
|
if (this->fadeThread != INVALID_HANDLE_VALUE) {
|
|
GetExitCodeThread(this->fadeThread, &this->fadeExit);
|
|
}
|
|
|
|
if (!this->fadePending || this->fadeExit != 0) {
|
|
return;
|
|
}
|
|
|
|
if (this->stream) {
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
miles.pauseStream(this->stream, 1);
|
|
miles.streamVolume(this->stream);
|
|
}
|
|
|
|
this->FadeIn();
|
|
this->RestartSecondary();
|
|
this->fadePending = 0;
|
|
}
|
|
|
|
// OFFSET: 0x006396B0
|
|
// FUNCTION: fn_45423
|
|
void CSoundMgr::Shutdown() {
|
|
if (this->m_context->configuration.mode == 0) {
|
|
return;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
|
|
if (this->provider) {
|
|
for (void*& sample : this->samples3D) {
|
|
if (sample) {
|
|
miles.release3DSample(sample);
|
|
sample = nullptr;
|
|
}
|
|
}
|
|
|
|
miles.close3DListener(this->listener);
|
|
miles.close3DProvider(this->provider);
|
|
this->provider = 0;
|
|
}
|
|
|
|
this->StopStreams();
|
|
|
|
for (void*& sample : this->samples) {
|
|
if (sample) {
|
|
miles.releaseSample(sample);
|
|
sample = nullptr;
|
|
}
|
|
}
|
|
|
|
if (this->driver) {
|
|
miles.closeDigitalDriver(this->driver);
|
|
this->driver = nullptr;
|
|
}
|
|
|
|
miles.shutdown();
|
|
}
|
|
|
|
// OFFSET: 0x00639880
|
|
// FUNCTION: fn_04753
|
|
void CSoundMgr::Destroy() {
|
|
if (g_soundMgr) {
|
|
delete g_soundMgr;
|
|
g_soundMgr = nullptr;
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x006398A0
|
|
// FUNCTION: fn_04754
|
|
void CSoundMgr::EndSamples() {
|
|
for (void* sample : this->samples) {
|
|
if (sample) {
|
|
MilesRuntime::Get().endSample(sample);
|
|
}
|
|
}
|
|
|
|
if (this->provider) {
|
|
for (void* sample : this->samples3D) {
|
|
if (sample) {
|
|
MilesRuntime::Get().end3DSample(sample);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x00639910
|
|
// FUNCTION: fn_04755
|
|
unsigned __stdcall CSoundMgr::FadeThread(void* direction) {
|
|
MSG message;
|
|
|
|
if (direction == reinterpret_cast<void*>(1)) {
|
|
for (int step = 1; step <= 1000; ++step) {
|
|
if (PeekMessageA(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE)) {
|
|
break;
|
|
}
|
|
|
|
CSoundMgr* sound = g_soundMgr;
|
|
const int volume = CSoundMgr::Volume(sound);
|
|
|
|
if (sound->targetVolume <= volume) {
|
|
break;
|
|
}
|
|
|
|
const int next = sound->streamVolume + 1;
|
|
CSoundMgr::SetVolume(sound, next);
|
|
Sleep(10);
|
|
}
|
|
} else {
|
|
for (int step = 1; step <= 1000; ++step) {
|
|
if (PeekMessageA(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE)) {
|
|
break;
|
|
}
|
|
|
|
CSoundMgr* sound = g_soundMgr;
|
|
|
|
if (!sound || sound->streamVolume <= 0) {
|
|
break;
|
|
}
|
|
|
|
const int next = sound->streamVolume - 1;
|
|
CSoundMgr::SetVolume(sound, next);
|
|
Sleep(10);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// OFFSET: 0x00639A60
|
|
// FUNCTION: fn_04756
|
|
int CSoundMgr::Volume(const CSoundMgr* sound) {
|
|
return sound ? sound->streamVolume : 0;
|
|
}
|
|
|
|
// OFFSET: 0x00639A70
|
|
// FUNCTION: fn_04757
|
|
bool CSoundMgr::IsComplete() const {
|
|
if (this->m_context->configuration.mode == 0 || !this->stream || !this->secondaryStream || this->fadeExit != 0) {
|
|
return false;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
|
|
if (miles.streamStatus(this->secondaryStream) == 2) {
|
|
return true;
|
|
}
|
|
|
|
return miles.streamStatus(this->stream) == 2;
|
|
}
|
|
|
|
// OFFSET: 0x00639AC0
|
|
// FUNCTION: fn_04758
|
|
bool CSoundMgr::IsStopped() const {
|
|
return this->stream && MilesRuntime::Get().streamStatus(this->stream) == 8;
|
|
}
|
|
|
|
// OFFSET: 0x00639AF0
|
|
// FUNCTION: fn_04760
|
|
void CSoundMgr::Pause(CSoundMgr* sound, int paused) {
|
|
if (!sound || !sound->stream) {
|
|
return;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
miles.pauseStream(sound->stream, paused);
|
|
miles.streamVolume(sound->stream);
|
|
|
|
if (paused == 0) {
|
|
sound->currentName = sound->defaultName;
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x00639B50
|
|
// FUNCTION: fn_45424
|
|
bool CSoundMgr::PlayData3D(const void* data, const float& x, const float& y, const float& z, int maximumDistance, int minimumDistance, float scale) {
|
|
const float scaledVolume = static_cast<float>(this->volume3D) * scale;
|
|
const int volume = TruncateToInt(scaledVolume);
|
|
|
|
if (!this->provider) {
|
|
return this->PlayData2D(data, x, y, z, 250, 40, 1.0f);
|
|
}
|
|
|
|
for (int i = 0; i < this->sample3DCount; ++i) {
|
|
if (!this->samples3D[i]) {
|
|
continue;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
|
|
if (miles.sample3DStatus(this->samples3D[i]) != 2) {
|
|
continue;
|
|
}
|
|
|
|
if (!miles.set3DSampleFile(this->samples3D[i], data)) {
|
|
return false;
|
|
}
|
|
|
|
miles.set3DSampleEffectsLevel(this->samples3D[i], this->effectsEnabled ? 1.0f : 0.0f);
|
|
miles.set3DPosition(this->samples3D[i], x, y, z);
|
|
miles.set3DOrientation(this->samples3D[i], -x, -y, -z, 0.0f, 1.0f, 0.0f);
|
|
miles.set3DSampleVolume(this->samples3D[i], volume);
|
|
miles.set3DSampleDistances(this->samples3D[i], static_cast<float>(maximumDistance), static_cast<float>(minimumDistance));
|
|
miles.start3DSample(this->samples3D[i]);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// OFFSET: 0x00639D30
|
|
// FUNCTION: fn_04761
|
|
void CSoundMgr::SetDefaultName(std::string name) {
|
|
this->defaultName = name;
|
|
|
|
if (this->m_context->serviceOptions.Get(7)) {
|
|
this->PlayStream(nullptr, 0, false);
|
|
}
|
|
|
|
this->SetRoom(0);
|
|
}
|
|
|
|
// OFFSET: 0x00639DF0
|
|
// FUNCTION: fn_04762
|
|
bool CSoundMgr::PlayData2D(const void* data, const float& x, const float& y, const float& z, int maximumDistance, int minimumDistance, float scale) {
|
|
const float zSquared = z * z;
|
|
const float xSquared = x * x;
|
|
const float horizontalSquared = xSquared + zSquared;
|
|
const float horizontal = std::sqrt(horizontalSquared);
|
|
const float horizontalResquared = horizontal * horizontal;
|
|
const float ySquared = y * y;
|
|
const float distanceSquared = ySquared + horizontalResquared;
|
|
const float distance = std::sqrt(distanceSquared);
|
|
|
|
if (distance >= static_cast<float>(maximumDistance)) {
|
|
return false;
|
|
}
|
|
|
|
int volume = this->sampleVolume;
|
|
const float minimum = static_cast<float>(minimumDistance);
|
|
|
|
if (!(minimum >= distance)) {
|
|
const float numerator = static_cast<float>(volume) * minimum;
|
|
const float attenuatedVolume = numerator / distance;
|
|
volume = TruncateToInt(attenuatedVolume);
|
|
}
|
|
|
|
const float scaledVolume = static_cast<float>(volume) * scale;
|
|
volume = TruncateToInt(scaledVolume);
|
|
|
|
for (int i = 0; i < this->sampleCount; ++i) {
|
|
if (!this->samples[i]) {
|
|
continue;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
|
|
if (miles.sampleStatus(this->samples[i]) != 2) {
|
|
continue;
|
|
}
|
|
|
|
miles.initSample(this->samples[i]);
|
|
miles.setSampleFile(this->samples[i], data, 0);
|
|
miles.setSampleVolume(this->samples[i], volume);
|
|
miles.startSample(this->samples[i]);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// OFFSET: 0x00639F00
|
|
// FUNCTION: fn_04763
|
|
void CSoundMgr::PlayStream(const char* name, int loopCount, bool fade) {
|
|
if (this->m_context->configuration.mode == 0) {
|
|
return;
|
|
}
|
|
|
|
if (!name) {
|
|
name = this->defaultName.c_str();
|
|
}
|
|
|
|
if (this->currentName == name) {
|
|
return;
|
|
}
|
|
|
|
if (fade) {
|
|
this->FadeOut();
|
|
this->fadePending = 1;
|
|
this->currentName.assign(name);
|
|
|
|
if (this->secondaryStream) {
|
|
this->reopenPending = 1;
|
|
return;
|
|
}
|
|
|
|
this->secondaryStream = MilesRuntime::Get().openStream(this->driver, this->currentName.c_str(), 0);
|
|
const std::string copiedName(name);
|
|
return;
|
|
}
|
|
|
|
this->StopStreams();
|
|
this->currentName.assign(name);
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
this->stream = miles.openStream(this->driver, this->currentName.c_str(), 0);
|
|
|
|
{
|
|
const std::string copiedName(name);
|
|
}
|
|
|
|
miles.setStreamVolume(this->stream, this->streamVolume);
|
|
miles.setStreamLoopCount(this->stream, loopCount);
|
|
miles.startStream(this->stream);
|
|
}
|
|
|
|
// OFFSET: 0x0063A130
|
|
// FUNCTION: fn_04764
|
|
void CSoundMgr::PlayWave(const char* name, float x, float y, float z, int maximumDistance, int minimumDistance, float scale, int unused) {
|
|
const unsigned char enabled = this->m_context->serviceOptions.Get(11);
|
|
|
|
if (this->m_context->configuration.mode == 0 || !enabled) {
|
|
return;
|
|
}
|
|
|
|
static constexpr char permittedName[] = "\xB9\xF6\xC6\xB0\xBC\xD2\xB8\xAE.wav";
|
|
|
|
if (this->unknown != 0 && std::strcmp(name, permittedName) != 0) {
|
|
return;
|
|
}
|
|
|
|
CWave* wave = static_cast<CWave*>(this->m_context->resources.Load(name));
|
|
|
|
if (!wave) {
|
|
return;
|
|
}
|
|
|
|
if (this->m_context->configuration.mode == 1) {
|
|
this->PlayData2D(wave->data, x, y, z, maximumDistance, minimumDistance, scale);
|
|
} else if (this->m_context->configuration.mode == 2) {
|
|
this->PlayData3D(wave->data, x, y, z, maximumDistance, minimumDistance, scale);
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x0063A3B0
|
|
// FUNCTION: fn_45425
|
|
void CSoundMgr::RestartSecondary() {
|
|
if (!this->secondaryEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (this->reopenPending && this->secondaryStream) {
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
miles.closeStream(this->secondaryStream);
|
|
this->secondaryStream = nullptr;
|
|
this->secondaryStream = miles.openStream(this->driver, this->currentName.c_str(), 0);
|
|
this->reopenPending = 0;
|
|
}
|
|
|
|
if (this->secondaryStream) {
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
miles.setStreamVolume(this->secondaryStream, this->streamVolume);
|
|
miles.setStreamLoopCount(this->secondaryStream, 1);
|
|
miles.startStream(this->secondaryStream);
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x0063A440
|
|
// FUNCTION: fn_04765
|
|
void CSoundMgr::SetSampleVolume(int volume) {
|
|
this->sampleVolume = volume > 127 ? 127 : (volume < 0 ? 0 : volume);
|
|
}
|
|
|
|
// OFFSET: 0x0063A470
|
|
// FUNCTION: fn_04766
|
|
void CSoundMgr::Set3DVolume(int volume) {
|
|
this->volume3D = volume > 127 ? 127 : (volume < 0 ? 0 : volume);
|
|
}
|
|
|
|
// OFFSET: 0x0063A4A0
|
|
// FUNCTION: fn_04767
|
|
void CSoundMgr::SetTargetVolume(int volume) {
|
|
this->targetVolume = volume > 127 ? 127 : (volume < 0 ? 0 : volume);
|
|
}
|
|
|
|
// OFFSET: 0x0063A4D0
|
|
// FUNCTION: fn_04768
|
|
void CSoundMgr::SetListenerYaw(float yaw) {
|
|
if (!this->listener) {
|
|
return;
|
|
}
|
|
|
|
const float angle = 180.0f - yaw;
|
|
const int degrees = TruncateToInt(angle);
|
|
const float sine = ClientSine(degrees);
|
|
const float cosine = ClientCosine(degrees);
|
|
MilesRuntime::Get().set3DOrientation(this->listener, sine, 0.0f, -cosine, 0.0f, 1.0f, 0.0f);
|
|
}
|
|
|
|
// OFFSET: 0x0063A560
|
|
// FUNCTION: fn_45426
|
|
void CSoundMgr::SetRoom(int room) {
|
|
if (!this->provider) {
|
|
return;
|
|
}
|
|
|
|
MilesRuntime& miles = MilesRuntime::Get();
|
|
this->roomSupport = miles.roomType(this->provider) != -1;
|
|
|
|
if (this->roomSupport) {
|
|
miles.setRoomType(this->provider, room);
|
|
}
|
|
}
|
|
|
|
// OFFSET: 0x0063A5A0
|
|
// FUNCTION: fn_04769
|
|
void CSoundMgr::SetVolume(CSoundMgr* sound, int volume) {
|
|
if (!sound) {
|
|
return;
|
|
}
|
|
|
|
sound->streamVolume = volume > 127 ? 127 : (volume < 0 ? 0 : volume);
|
|
|
|
if (sound->stream) {
|
|
MilesRuntime::Get().setStreamVolume(sound->stream, sound->streamVolume);
|
|
}
|
|
|
|
if (sound->secondaryStream) {
|
|
MilesRuntime::Get().setStreamVolume(sound->secondaryStream, sound->streamVolume);
|
|
}
|
|
}
|