合并本地领先源码:旧UI、GM疲劳燃烧、白金徽章孔、宠物修复、支援兵普攻开关

This commit is contained in:
hanfuit committed 2026-11-04 19:20:49 +08:00
1 parent f2debaf9bf
commit 70b3c5a6dc
57 files changed
+4256 -30

No files matched your search

+44
View File
@@ -261,6 +261,14 @@ bool CompanionEquipmentProbeEnabled() {
return enabled;
}
// 支援兵普攻开关:启动器在客户端目录放置 90CN_basic_attack.disabled。
// 只影响 CompanionBasicAttack 回退程序,12 格技能程序不变。
bool CompanionBasicAttackEnabled() {
static const bool disabled =
CompanionFlagFilePresent(L"90CN_basic_attack.disabled");
return !disabled;
}
bool CompanionEquipmentApplyEnabled() {
// Production default: the mode1 equipment rows are replayed for every
// companion APC. A disable flag exists for a one-restart rollback.
@@ -1924,6 +1932,42 @@ bool ApplyDynamicRuntimeKeyPrograms(
return false;
}
// 关闭普攻时把 CompanionBasicAttack 的技能码改成空占位码,
// 只禁用技能冷却后的普攻回退,不触碰 0..11 的 12 格技能程序。
if (!CompanionBasicAttackEnabled()) {
void* basicProgram =
programs[Contract::kBasicAttackProgramIndex];
std::uint8_t* basic =
reinterpret_cast<std::uint8_t*>(basicProgram);
if (basic) {
std::uintptr_t basicBegin =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeBeginOffset);
std::uintptr_t basicEnd =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeEndOffset);
std::uintptr_t basicCapacity =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeCapacityOffset);
if (basicBegin && basicEnd >= basicBegin &&
basicCapacity >= basicEnd &&
basicEnd - basicBegin >=
g_native.kRuntimeKeySkillCodeOffset + 4) {
*reinterpret_cast<std::uint32_t*>(
basicBegin +
g_native.kRuntimeKeySkillCodeOffset) =
Contract::kSkillCodeBias +
Contract::kPlaceholderSkillId;
Log("companion basic attack disabled actor=%p program=%p",
actor,
basicProgram);
}
}
}
std::uint8_t* directBytecode[
Contract::kQuickSlotCount] = {};
std::size_t directBytecodeSize[
+95
View File
@@ -0,0 +1,95 @@
// Included inside 90CN.cpp's namespace after its existing patch helpers.
// Only the evidenced UI update iterator call-site is patched, never the CRT.
uintptr_t g_uiIteratorNativeAdvance = 0;
uintptr_t g_uiIteratorAdvanceResume = 0;
uintptr_t g_uiIteratorNextBucket = 0;
volatile LONG g_uiIteratorInvalidations = 0;
int __stdcall CanAdvanceUiUpdateIterator(unsigned int bucket,
const uintptr_t* owner, const uintptr_t* iterator)
{
dnf90::UiIteratorSnapshot s = {};
__try {
s.owner = reinterpret_cast<uintptr_t>(owner);
if (owner && iterator) {
s.proxy = owner[0]; s.iteratorProxy = iterator[0];
s.begin = owner[1]; s.end = owner[2]; s.current = iterator[2];
if (s.proxy && s.iteratorProxy == s.proxy)
s.proxyOwner = *reinterpret_cast<const uintptr_t*>(s.proxy);
}
} __except (EXCEPTION_EXECUTE_HANDLER) { s.proxyOwner = 0; }
if (dnf90::CanAdvanceUiIterator(s)) return 1;
LONG n = InterlockedIncrement(&g_uiIteratorInvalidations);
if (n <= 16) LogLine("ui-update iterator invalidated bucket=%u owner=%08X "
"proxy=%08X iter_proxy=%08X begin=%08X end=%08X current=%08X "
"action=defer_remaining_bucket_until_next_frame hit=%ld",
bucket, s.owner, s.proxy, s.iteratorProxy, s.begin, s.end, s.current, n);
return 0;
}
__declspec(naked) void ProxyUiUpdateIteratorAdvance()
{
__asm {
// Entry is a JMP replacing CALL; result pointer and dummy are on stack.
push eax
push ecx
push edx
push ecx
push dword ptr [ebp - 14h]
push dword ptr [ebp - 1Ch]
call CanAdvanceUiUpdateIterator
test eax, eax
pop edx
pop ecx
pop eax
jz invalidated
call dword ptr [g_uiIteratorNativeAdvance]
jmp dword ptr [g_uiIteratorAdvanceResume]
invalidated:
// Native ++ would have consumed two arguments (ret 8). Do not compare
// an invalid iterator again; the next bucket constructs a fresh one.
add esp, 8
jmp dword ptr [g_uiIteratorNextBucket]
}
}
bool InstallUiIteratorAdvanceCompatibility()
{
const uintptr_t site = g_dnfBase + 0x02BF2732;
static const unsigned char kLoopContract[] = {
0x8B,0x0F,0x8B,0x11,0x8B,0x42,0x10,0xFF,0xD0,0x6A,0x00,
0x8D,0x4D,0xB4,0x51,0x8D,0x4D,0xD4,0xE8,0x19,0x02,0x7F,0xFD,
0xE9,0x77,0xFF,0xFF,0xFF,0x8B,0x45,0xE4,0x83,0x45,0xEC,0x14,
0x40,0x89,0x45,0xE4,0x3D,0x87,0x04,0x00,0x00,0x0F,0x8C,0x55,0xFF,0xFF,0xFF
};
static const unsigned char kBeginContract[] = {
0x55,0x8B,0xEC,0x8B,0x45,0x08,0x8B,0x51,0x04,0x8B,0x09,
0xC7,0x40,0x04,0,0,0,0,0x89,0x50,0x08,0x89,0x08,0x5D,0xC2,0x04,0
};
static const unsigned char kAdvanceContract[] = {
0x55,0x8B,0xEC,0x8B,0x45,0x08,0x8B,0x11,0x56,0xC7,0x00,0,0,0,0
};
if (!BytesMatch(reinterpret_cast<unsigned char*>(g_dnfBase+0x02BF2720),
kLoopContract,sizeof(kLoopContract)) ||
!BytesMatch(reinterpret_cast<unsigned char*>(g_dnfBase+0x00A44ED0),
kBeginContract,sizeof(kBeginContract)) ||
!BytesMatch(reinterpret_cast<unsigned char*>(g_dnfBase+0x003E2950),
kAdvanceContract,sizeof(kAdvanceContract))) {
LogLine("ui-update iterator compatibility disabled: native signature mismatch");
return false;
}
g_uiIteratorNativeAdvance = g_dnfBase + 0x003E2950;
g_uiIteratorAdvanceResume = g_dnfBase + 0x02BF2737;
g_uiIteratorNextBucket = g_dnfBase + 0x02BF273C;
DWORD oldProtection = 0;
if (!VirtualProtect(reinterpret_cast<void*>(site),5,PAGE_EXECUTE_READWRITE,&oldProtection)) {
LogLine("ui-update iterator compatibility protection failed error=%lu",GetLastError());
return false;
}
unsigned char jump[5] = {0xE9};
*reinterpret_cast<int32_t*>(jump+1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(&ProxyUiUpdateIteratorAdvance)-static_cast<intptr_t>(site+5));
memcpy(reinterpret_cast<void*>(site),jump,sizeof(jump));
DWORD ignored = 0;
VirtualProtect(reinterpret_cast<void*>(site),5,oldProtection,&ignored);
FlushInstructionCache(GetCurrentProcess(),reinterpret_cast<void*>(site),5);
LogLine("ui-update mutation-safe iterator installed site=%08X next_bucket=%08X",site,g_uiIteratorNextBucket);
return true;
}
@@ -0,0 +1,703 @@
'� � �
�
�
�
�
�
�
�
� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �#%� � � � � � � � � � � � � �뇦�MNN�MNN�MNN�utr�utr�utr�MNN�MNN�MNN�MNN�뇦�
� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �  � �뇦�MNN�MNN�MNN�MNN�utr�utr�utr�MNN�MNN�MNN�뇦� � � � � � � � � � � � �뇦�
� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �뇦� � � � � � � � � � � � ���撞�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������utr�utr�MNN�MNN�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�MNN�MNN�MNN�MNN�MNN�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�MNN�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�MNN�MNN�MNN�MNN�MNN�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�MNN�MNN�utr�utr� �MNN�MNN� �utr� � �utr� � � � � � �MNN� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �MNN� � � � � � �utr� � �utr� �MNN�MNN� � �뇦� �MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN� � �蔽$�遡q�蔽$�蔽$�蔽$�駭)�駭)�駭)�蔽$�駭)�駭)�駭)�駭)�駭)�蔽$�駭)�駭)�蔽$�蔽$�蔽$�蔽$�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�蔽$�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�蔽$�蔽$�蔽$�蔽$�駭)�駭)�蔽$�駭)�駭)�駭)�駭)�駭)�蔽$�駭)�駭)�駭)�蔽$�蔽$�蔽$�遡q�蔽$� � �MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN�MNN� �뇦� �MNN�utr�MNN�MNN� � � � � � � � � � � �蔽$�駭)�駭)��+��+�棋_�棋_�棋_�棋_��+��s*��s*��s*��s*��x,��+��s*��s*��+��+��+��x,��+��+��s*��x,��x,��+��s*��+��s*��+��x,��x,��+��+��+��+��+��+��s*��s*��+��+��+��+��+��x,��s*��s*��+��x,��+��+��s*��s*��+��+��+��s*��+��x,��x,��s*��+��+��x,��+��+��+��s*��s*��+��x,��s*��s*��s*��s*��+�棋_�棋_�棋_�棋_��+��+�駭)�駭)�蔽$� � � � � � � � � � � �MNN�MNN�utr�MNN� �MNN� �MNN� �蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�駭)�駭)��+��+��s*�쮟3�똣4�냍0�냍0�냍0�냍0�똣4�켷G�켷G���������oP1�����냍0�냍0�냍0�냍0�냍0�냍0�냍0�����냍0�냍0�냍0�냍0�����냍0�oP1�냍0�냍0�냍0�냍0�����냍0�냍0�냍0�냍0�����냍0�����냍0�냍0�냍0�냍0�냍0�냍0�냍0�oP1�oP1�����냍0�냍0�냍0�냍0�냍0�냍0�oP1�냍0�����냍0�냍0�냍0�냍0�냍0�냍0�냍0�냍0�냍0�����냍0�냍0�냍0�����냍0�똣4�켷G�켷G�똣4�냍0�냍0�냍0�냍0�똣4�쮟3��s*��+��+�駭)�駭)�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$�蔽$� �MNN� �MNN� � �뇦� �MNN� �蔽$�蔽$�駭)��+��+��+��+��s*�쮟3�쮟3�냍0�oP1�M,�M5(�M5(�M5(�M5(�M5(�M5(�������������M5(�M5(�����M5(�0$�����M5(�M5(�M5(�����M5(�M5(�M5(�M5(�M5(�����M5(�M5(�M5(�M5(�M5(�M5(�����M5(�M5(�M5(�M5(�����0$�M5(�����M5(�M5(�0$�M5(�����M5(�M5(�M5(�����0$�M,�0$�M5(�M5(�M5(�M5(�M5(�����M5(�M5(�M5(�M5(�M5(�M5(�M5(�M5(�M5(�����0$�����������������������������M5(�M5(�M5(�M5(�M5(�M5(�M,�oP1�냍0�쮟3�쮟3��s*��+��+��+��+�駭)�蔽$�蔽$� �MNN� �뇦� �MNN�뇦� �MNN� �蔽$��+��s*�쮟3�냍0�냍0�냍0�냍0�sK�M,�M5(�,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�����,**�,**����� � � �����,**���������������������,**�,**�����������������,**�,**�,**�������������,**�,**����� �,**�,**�,**�,**�,**� �,**����� �,**�����,**� �,**�,**�,**�,**�,**�,**�����,**�,**� �,**�,**�,**�,**�,**����� � � �,**�����,**� �,**�,**�,**�,**�,**�,**�,**�,**�,**�,**�M5(�M,�sK�냍0�냍0�냍0�냍0�쮟3��s*��+�蔽$� �MNN� �뇦�MNN� �MNN�MNN� � �駭)��s*�냍0�YF/�M5(�M5(�YF/�0$�M5(�,**�,**�) �,**�,**�,**� �,**�,**�,**�,**�,**�����,**� �����,**� �,**� � �����,**�,**�,**�����,**�����,**�,**�,**�����,**� � �����,**�,**�,**�,**�����������������,**�,**�,**�,**�,**�,**�����,**�����,**�,**� �,**�����������������������������������������,**�,**� �����,**� �������������������������,**�,**�,**�,**� �,**�,**�,**�) �h딧�h딧�M5(�0$�YF/�M5(�h딧�h딧�냍0��s*�駭)� � �MNN�MNN� � �MNN� � �蔽$��+�냍0�M5(�,**� �,**�,**� �,**� �,**�,**�,**�,**� �,**� �,**��������������������������������������������� �����,**�,**�,**���������,**�,**�,**�,**�����,**�,**�,**�����,**�,**������������� �,**�,**� �,**�����,**�,**�,**�,**�,**����� �,**�,**�,**�,**�,**�,**�,**�����,**�,**�,**�,**�����,**�,**Line truncated
�
�
�
�%FO�U「�Q넄�+"�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �0�0�2!�2!�2!�2!�2!�0�/�0�0�*�*�*�*�*�*�*�(�*�(�+�+�(�(�(�*�*�*�(�(�(�*�*�,�(�(�(�*�*�(�*�(�*�*�*�*�*�*�*�(�*�(�+�+�(�(�(�*�*�*�(�(�(�*�*�,�(�(�(�*�*�(�*�(�*�*�*�*�*�*�*�(�*�(�+�+�(�(�(�+�[윸�2`m�
�
)Z�Q�b�b�Q�
)Z�
�2`m�[〕�+�)�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �/�0�2!�2!�2!�0�0�0�0�0�0�/�,�,�*�*�*�*�(�*�(�*�+�(�*�*�*�*�(�(�(�(�(�*�*�+�+�(�*�*�(�*�(�*�*�*�*�*�*�*�(�*�(�*�+�(�*�*�*�*�(�(�(�(�(�*�*�+�+�(�*�*�(�*�(�*�*�*�*�*�*�*�(�*�(�*�+�(�*�)�P꼺�5dr� �O� V�.`����,]�O� �5dr�R꼹�*�*�*�*�*�+� �뇦� �MNN�MNN� �뇦� �/�0�0�0�0�0�0�0�2!�2!�0�0�0�-�-�,�(�(�(�(�(�(�(�(�(�(�(�(�*�*�*�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�*�*�*�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�365�U「� �L� k�-\�뀶�븝�븝�븝��:y�L� �U「�587�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �/�/�/�0�2!�2!�2!�0�2!�2!�0�0�/�/�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�O걥�%FO�
)Y�a�?��e깒�븝�Ldk��샗�⒩� �a�
)Y�#BL�O걤�+�+�*�+�+� �뇦� �MNN�MNN� �뇦� �/�0�/�0�2!�2!�2!�0�0�/�/�0�0�-�,�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�b꽈�
�J�
a�6�샗�샗�6�4i�e깒�븝��
a�J� �b꽃�+�*�*�*�+� �뇦� �MNN�MNN� �뇦� �-�0�0�0�0�0�0�0�/�/�0�/�/�-�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�b밉�
�R�Z�:w�1�1�#J��⒩�샗�1�Z�R�
�b밉�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �0�0�2!�2!�0�0�0�/�0�0�/�,�,�,�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�E��
�N� R�K�K�3j�Ldk�븝�샗�,�3k� R�M��C}�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �0�0�2!�2!�0�0�0�/�0�0�/�-�,�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�6Zi��B� K�C�A�(�샗�샗�(�,_�C� K�B��6Yg�+�*�*�+�+� �뇦� �MNN�MNN� �뇦� �,�/�/�/�/�0�/�/�/�/�-�-�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�2EN�#)�
$Q�E�>�8}�%T�#��D�8}�>�E�
$Q�!'�2EM�*�*�+�+�+� �뇦� �MNN�MNN� �뇦� �-�/�/�0�0�0�0�/�-�-�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�,�,�,�,�+�+�+�+�+�+�+�+�*�*�*�*�+�+�*�+�+�+�+�,�+�,�,�,�,�,�,�,�+�+�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�-*'�,Q`� �<� @�7|� !�샗�⒩��7|� @�<� �,Q`�-)%�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �/�-�/�/�0�0�/�/�-�/�-�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�+�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�*�*�*�)�3KS�5?�
�9�=�%�샗�⒩��=�9�
�5?�4LT�+�+�+�+�*�+� �뇦� �MNN�MNN� �뇦� �/�-�,�-�-�/�/�/�/�/�-�,�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�+�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�*�*�*�*�+�+�+�*�*�*�*�*�*�*�)�)�9\h�6?� �
!L� &X�)�� H�
!L� �6?�8]i�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �-�-�,�,�-�/�/�/�-�,�,�,�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�*�*�+�+�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�*�*�*�*�*�*�*�*�*�*�*�*�*�)�)�*�7Q[�3_p�)0��� ��)0�3_p�8T^�+�+�+�+�+�+�*�+� �뇦� �MNN�MNN� �뇦� �-�-�-�,�,�,�,�,�,�,�,�*�*�*�*�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�%!#�(9�,L� 0Z�2g�6p�8v�:|�:|�8v�6p�3g� 1[�,K�'9�# "�*�*�*�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�*�+�)�*�*�)�,+(�8S]�>o�;p�;n�>m~�8S]�.-*�+�+�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �,�-�-�/�0�0�/�(�(�-�/�/�/�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�'�"/�,R�5p�M�.g�A{�F궹�K눻�O뚒�J뗓�O뤫�W쇹�U쀠�Y쇸�V뺎�P띝�D��0j�M�5p�,R�"/�'�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�(�(�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �,�,�,�-�/�0�/�-�(�,�,�,�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�#�*I�;~�$_�D�M뚒�J뤫�I롕�J먝�J믛�F띧�I뤬�E}�2K�2K�P눬�Y쒋�Z싞�X싟�X쎺�]잋�^졿�\쒊�K눵�$_�;~�*I�#�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�(�(�)�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �-�-�,�,�-�-�/�/�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�%#)�3W�N�B}�Q먚�C눾�A댲�J볖�J뫽�N볖�R쀟�M먝�F듫�G됓�+Ow�3O�3O�(Mw�3|�.x�,v�-w�.x�2w�:��G듫�S붭�X뽡�C~�J�-R�$"(�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�-�,�+�,�+�+�*�*�*�*�*�*�+�+�+�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �,�-�-�.�,�,�,�,�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�)!"�0S�X�?j�Fu�F눾�@됖�L붯�S쀠�R볓�G댮�<��4x�.u�.x�5~�5f�!>_�!>_�8i�G롗�F롖�O뽤�L붱�G먟�A뗙�9녧�2~�5��3z�B끦�[샽�N댪�R�.P�("#�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�-�,�-�,�,�*�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �,�-�-�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�*F�A�E�<d�)@�)@�6e�L믙�N뫺�C꺧�1u�)n�+n�!]�M�?u�
5h�<m�Bn�Bn�<m� 6h�@u�P�*b�9z�J먝�J믛�M붱�N뺖�N뺖�6c�(>� ;\�L눵�C{�A�*F�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�-�,�-�,�,�*�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �,�-�-�+�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(� )�,S�(b�U믗�P뤫�+Nw�9W�8V�2W�B꺨�/s� \�G��(W�-]�2d� 7i�:n�<q�?s�Cx�Cx�?s�<q�:n� 7i�2d�-]�(W�H��.i�H뚓�T섁�1K�6R�1L�4e�5{�E꾅�(b�,S� )�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�(�(�)�)�*�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �+�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�*?�?�B~�O뤫�@꺫�;꺪�B}�)Jp�)Kr�;p�Q�)X�0`� 8j�>q�@t�Au�Au�Av�Bv�Bv�Bw�Bw�Bv�Bv�Av�Au�Au�@t�>q� 8j�0`�)X�.c�-Ou�"Ad� =\�O됅�L먟�>눿�>끨�=y�?�*?�,�,�,�,�,�,�,�,�,�,�,�,�,�+�*�+�*�+�)�*�*�*�*�*�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�+�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�+H�J�Q롍�P먛�>곉�9곍�G띢�J롒�I끟�!V�1`� 5f�=q�?t�@v�Aw�Bw�By�Cy�Cz�Dz�D{�D{�D{�D{�Dz�Cz�Cy�By�Bw�Aw�@v�?t�=q�9j�=m�/Z�H~�L먝�H뚓�P볔�M뫾�G띥�D꺥�J�+H�*�*�*�*�*�*�*�*�*�*�*�*�)�)�*�)�)�(�)�)�)�)�)�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)H�T�S믖�Q뫸�>곉�4|�L먜�M롐�"V�*X�
4e�>r�@t�Av�Bw�Cx�Dz�Cp�!AR�)>6�/<#�3:�69 �88�88�69 �3:�/<#�)>6�!AR�Cp�Dz�Cx�Bw�Av�Au�@t�9j�.]�&[�K뚒�G뚒�O믙�T쀝�D뚕�C꾅�S�)H�(�(�(�(�(�(�(�(�(�(�(�(�(�)�(�(�'�'�(�(�(�(�'�'�'�'�'�'�'�'�'�'� �뇦� �MNN�MNN� �뇦� �)�)�+�-�/�0�/�/�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)H�V�\쇵�Q믗�H듨�9곋�B댲�=|�N�/_�;o�?s�@u�Bw�Cx�Cq�$?D�1;�66�01�+-�'*�$'�"%�!%�!%�"%�$'�'*�+-�01�66�1;�$?D�Cq�Cx�Bw�@u�?s�<o�/_�I�:v�K롒�T쀞�S쀞�N믙�E녟�U�)H�)�)�)�)�)�)�)�)�)�)�)�)�*�(�)�' �' �' �' �' �' �' �' �' �' �' �' �' �' �' �' � �뇦� �MNN�MNN� �뇦� �,�,�,�+�0�0�/�-�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�+G�V�\싛�V쀝�M롏�4~�J뫻�.f�+X� 5e�>q�?s�Au�Bw�Ah�'=7�66�./�&(�$&�13�@@�JI�SP�XU�[X �[X �XU�SP�JI�@@�23�$&�&(�./�66�(=4�Ae�Bw�Au�?s�>q� 5e�+X�.e�O믗�Y싞�\쎷�P붫�>��S�)E�.�,�.�.�,�,�,�,�,�,�*�+�*�+�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �-�+�-�-�/�/�/�-�)�)�)�)�(�(�(�)�)�)�)�)�)�)�)�)�)�)�)�%;�Q�O궩�@j�T먓�9�G뤪�.c�,Y�9j�>q�?s�Au�Aq�&<8�54�+-�"&�24�HG�_\ �rn�뀅%�뵍1�쥮>�エH�괸O�괵M�ぇE�젦;�뵍0�뀅%�ro�_\ �HG�24�"&�*,�54�&<8�Aq�Au�?s�>q�9j�,Y�.b�S볊�^왺�_졻�H뚑�J댫�L�&;�)�)�)�)�)�)�)�)�)�)�*�' �'�' �' �' �' �' �' �' �' �' �( �(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �-�/�+�/�/�0�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�%� C�Q꽴�+B�+B�!<[�@됕�-a�,Z�:k�=q�?s�Av�!=H�66�-.�#&�9:�\Y
�uq�뼇8��J�병[�좀j�횬v�股�路�畓�畓�騏�箝�퓻t�윽e�릴U��G�븧5�so�\Y
�9:�#&�-.�66�!=H�Av�?s�=q�:k�,Z�/a�^앦�.Ln�%9�%Cf�M녚�A�$�(�(�(�(�(�(�(�'�(�)�' �'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'� �뇦� �MNN�MNN� �뇦� �0�0�-�/�0�0�0�.�.�/�/�/�.�.�.�.�.�.�.�.�.�.�(�)�)�(�+Q�L끚�Hy�6S�;[�6Q�7n�,Z�:l�=p�?s�@u�%;8�23�&)�46�PO�}y'�│I�뭅^�컁q�膏�驥�畓�沓�畓�頓�錟�錟�沓�露�錟�騏�邱�愷|�쩌m�럴X�짛D�zw$�NM �46�&)�23�%;7�@u�?s�=p�:l� .]�7f�0J�7U�0J�:s�D~�,Q�(�)�+�(�(�(�)�)�*�,�*�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,� �뇦� �MNN�MNN� �뇦� �2!�0�2!�2!�0�0�0�0�/�/�0�/�/�+�+�+�+�+�+�.�+�.�(�)�,�'C�*d�R믖�Y쀚�5Z�'In�5a�7d�8i�<o�>q�@t�&:2�/0�$(�BC�li�줉G�섯e�珏�駑�路�路�根��匡y�訣q�迦e�퓟]�탁X�타Y�횐_�介g�皐p�光y�根��饑�騏�芹�프x�뭅\�쐶>�hd�BC�$(�/0�&:2�@t�>q�<o�9k�>k�&Eg�#Be�(Hk�D됑�<~�*c�(D�(�(�+�(�)�.�)�)�+�(�,�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �2!�0�2!�2!�2!�0�0�0�0�0�/�-�(�(�(�(�(�(�)�+�.�.�(�)�(!#�@�S뫵�M뫹�P믖�@끥�?겾�5b�8i�;n�=q�?s�%80�-.�*,�GF ��.�뎡]�첸r�謙�慤x�퓸n�탈i�캡b�첨[�쨉V�천S�척L�캑H�척G�처G�척I�캑K�캔Q�천U�첨Z�캣`�탈g�퓸o�慤x�愷}�은j��P�{x$�ED�*,�-.�%80�?s�=q�<o�<m�>k�Gz�`쒃�R샽�-v�I끠�@�"�(�)�(�)�.�)�)�*�+�*�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �2!�0�0�0�2!�/�/�0�0�/�-�)�)�)�)�(�(�(�(�)�+�.�(�)�-P�Az�R볎�Y싙�@끥�:꺩�'X� 3b�:l�<o�>q�$81�,.�.1�c`�몠<�릿^�커s��w�캥j�윳\�쓩T�쉼P�쉴L�쒼J�육F�윤C�챵@�캐=�캐;�캑<�캑<�처=�짼@�윤D�엽H�쒼K�쒼N�엾U�융^�캬j�횰v�쩌l�눕T�뙃3�_\�.1�,.�$81�>q�<o�;m�5d�)Z�Y싛�U샽�:꺪�2t�Bz�,O�)�(�(�)�.�)�)�+�*�(�(�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �0�0�2!�0�0�/�0�0�/�-�+�/�)�)�)�)�)�)�(�(�+�.�(�!&�L�R뫷�N먙�J뚎�7�,a�
/\�9k�;m�=p�#75�-/�-1�da�뿎C�릿_�첵n�청i�쉿X�변O�벽M�벼K�벵K�벵G�쉰C�쒜@�육<�琡�鋏�琡�틂1�博�町�穆�유<�쒜A�서E�벵H�벼K�벽M�변N�쓴[�청k�좀k�눗V�뮀9�`^�-1�-/�#75�=p�;m�9k�
/\�2e�X싙�D뗕�)q�I꾁�L�%�)�(�)�,�)�)�+�(�(�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �0�/�0�0�0�0�0�/�/�/�/�/�-�+�(�(�)�)�)�(�+�.�(�,Q�E�S붪�S볊�5z�A궶� -X�6f�9k�<n�8C�./�(+�QP�뵎B�뎠Z�슐a�섧\�릭P�랸K�략H�랴G�랴F�랴D�릉B�벴>�쉭9�졈:�莢�和�和�홴)�杭�和�標�졀9�쉭:�벴?�릊B�랴E�랴G�략H�릎K�묻Q�섧]�섬`�낟U�룎:�LK�&)�./�8C�<n�9k�6f� -X�O띘�P뺎�6곋�+n�D|�,Q�(�(�)�,�)�)�+�(�)�)�)�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �0�0�0�0�/�0�/�/�0�/�-�-�+�*�*�(�(�)�)�(�+�.�#�I�R뫵�T뺊�J뚎�>꺧�+]� 1_�7i�9l�:j�//�+-�MM�냳7�괸S�럭W�묻S�땝I�뎀F�델D�뇽C�뇻B�뇻A�뇹@�덴=�릇:�섕6�횐P�迪�薑8�外�홴%�迹�聃Y�桎�획M�뺄6�릇:�덴>�뇻@�뇻B�뇽C�델D�뎀F�땝K�별V�럭W��O��~/�II�(+�//�:i�9l�7i� 1_�/_�Y쎵�I뤪�(o�D�I�#�(�)�,�)�)�,�(�)�)�(�(�(�)�)�)�)�(�(�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �2!�0�0�/�/�/�/�/�-�+�+�(�(�(�(�*�(�)�)�(�+�.�-M�G�V뺎�P뫷�2u�A궴� .X�5e�8i�9l�"32�-.�AB�uq)�ЖN��K�눋K�꺽F�괜C�괘@�괘?�광>�괏>�괏=�괏<�낄:�때7�벳2��R�蕁e�蒼�澧\��"�襪b�音�栗p��R�벱2�덱7�낄:�괏<�괏>�광>�괘?�괘@�괜C�꺽F�뎅M��K�ぇL�pl"�<=
�,-�"32�9l�8i�5e� .X�V볂�Q붫�+t�3r�By�,K�(�)�,�)�)�,�(�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �/�0�0�/�-�/�/�-�-�+�(�(�(�(�(�*�(�)�)�(�+�- �K�3T{�6P�:e�9~�*]�1^�6g�8j�7[�,,�/3
�][�쐶C�ªA�괬G��C��@��=��<�췅:�췃:�췃9�췃9�츪8�가6�끽3�름/�潔Z�暾e�和�背o�� �朗Z�和�槁O�鱇Z�름/�끽3�가6�츪8�췃9�췃:�췅:��<��>��@��C�괬I�ĦC�슄?�XV�+.�,,�7Z�8j�6g�1^�4e�Y쒆�?w�'Lu�Dq� E�'�)�.�)�)�+�(�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �/�/�/�/�-�-�+�+�-�-�+�(�)�(�(�*�(�)�)�(�+�#)7�/]�.G�3O�.F�I뗉�/X�4c�6h�8k�"1.�-.�II�{.�ℓC�ぃA��A�А=�쳽:�첑8�찟6�찞6�찞5�찞5�찞5�찞3�쵞2�가/�땋+�痼a�和�出�和�풍�和�和�和�缺_�땅+�가/�쵞2�첏4�찞5�찞5�찞6�찟6�첑8�쳽:�А=��B�イC�ℓC�|x)�CD �,.�"1-�8k�6h�4c�/X�]샭�-D�.G�,B�0b�#0�)�,�)�)�+�(�)�)�(�(�)�(�)�)�(�)�)�)�)�)�(�(�)� �뇦� �MNN�MNN� �뇦� �/�/�/�-�/�-�-�-�+�+�(�)�)�(�(�*�(�)�)�(�+�1W�K}� <[�"@b�$Ac�:t�1[�4d�6g�7b�++�03
�gc�쐷?�짗;�ィ@�쯃<�쯀8�쬄6�쬂4�쪠3�쪟2�쪟2�쪟2�쪟2�쬁0�쮾/�췂,�끽(�錤n�箱�홴-�緣�틀�蔭�풩*�軾�懃l�끽(�췂,�쮾/�쬂0�쪟2�쪟2�쪟2�쪠3�쬂4�쬄6�쯀8�쯃<�ィA�쩅<�썥>�b_�-/�**�7b�6g�4d�1[�L�6T�!>_�6T�Fw�-S�)�,�)�)�+�(�)�)�(�(�)�(�)�)�*�(�)�)�)�)�(�(�)� �뇦� �MNN�MNN� �뇦� �-�-�-�-�+�+�,�(�(�*�(�(�(�(�(�*�(�(�)�(�+�7p�[볆�G��=l�>y�*V�2^�5e�7g�1;�--�AB�wu'�쩆?�짔9�쭫<�쪢7�짌3�쥥1�죾0�죻/�죻/�죻/�죻/�죻/�짉-�쪟,�쳺*�깻&�勒��決O�替�蠟W�틂�澧]�足�痼T�隷}�깼&�쳺*�쬂+�쥤-�죻/�죻/�죻/�죻/�죾0�쥥1�짌3�쪢7�쭫;�쩄:�짘>�vr#�=> �+,�1;�7g�5e�3_�1]�Gx�-V�4b�[붥�6o�)�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�(�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �+�-�,�,�+�,�*�*�*�*�*�-�-�+�*�*�(�(�)�(�%"�K�R먖�B눻�N띜�.o�0X�3a�5e�6g�$*�.0�TR�럩3�윓7�쩃9�짎5�죾2�윍/�옊-�쓳,�쓳,�쓳+�옉,�옉,�쓳+�젗*�짉)�첎'�깻#�璹�槁M�杭�例[�틀�隙D�杭�枯L�屑�깻#�첎&�짉)�윋)�쓳*�옉,�쓳,�쓳,�쓳,�옊-�윍/�죾2�짎5�쩃:�윓7�뜈2�QO�-/�$*�6g�5e�3a�1Z�M뤧�8~�*j�T뫳�K�#�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�+�-�/�/�-�(�+�+�(�(�*�(�(�)�(�&8�-e�N뚎�K뤧�I듡�+i�0Y�2b�4e�5^�((�36�ea�윖<�쓼5�줂6�윎1�쐭-�쉹*�쉸)�쉷(�쉷(�쉷(�솉(�쉷(�썚'�쐫&�죺%�찚"�뇩�珽�枯?�抗�抗�튿�抗�抗�暠@�迹�뇩�첍#�쥟%�쓲&�썚'�솉(�솉(�쉷(�솉(�쉸)�쉹*�쐮-�윎1�줃6�쓼5�윖<�c_�14�''�4\�4e�2b�0Y�C�3}�a�N뚌�-d�$5�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�)�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�+�-�+�+�+�(�(�(�(�*�(�)�)�(�+K�Ay�G됍�P믔�A꾁�([�0[�2b�4e�/C�*+�@A �so �씂7�쐳2�옏2�쉻,�삇)�뼂'�븡&�븡%�븡%�븡%�븡%�븡$�뿃#�쐪#�죺!�찚�쒜3�標�講:�標�標�짯�標�標�鵑=�標�쉭0�쳹�죺!�썜#�뿃$�븡$�븡%�뻿%�뻿%�븡&�뼂'�삆)�쉻,�옏2�쐳3�씂7�rn�=>
�)*�/B�4e�2b�0[�9j�;곉�"d�H꽾�@w�)H�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�)�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�,�+�,�,�)�)�(�(�*�(�)�)�(� 0[�M녍�D눸�L뤧�9}�N�1\�3c�4e�+-�--�KJ��~(�솗4�슂2�솑.�븤*�뱣&�뭸$�몘$�몘$�몘$�몘$�몘#�뭸#�븞 �뿂�쓯�첌�狼r�跛�抉B�跛�跛�젱�跛�跛�횅5�跛�瑙j�찙�윇�솉�븠#�뭷#�몘#�몘$�몘$�몘$�뭸$�뱣&�븣*�솑.�슂3�슄5��~(�IH �+,�+-�4e�3c�1\�.Z�?녟�_�@|�N녎� /Z�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�)�(�)�)�(�(�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�+�-�-�-�+�)�(�(�*�(�)�)�(�2f�T뚇�H듡�K뤥�.r�Es�1]�3c�5e�)�.0�RP�땲.�뿋2�삊/�븤+�몚&�럢#�뛽"�뛼 �뛼 �럟 �룂 �룂���럞�뿁�쨺�뇨�音�坼�坼�坼�坼�윕�坼�坼�坼�坼�羽�깹�짃�뿁�몗�몖�릢�럠 �럟 �뛼 �뛽!�럢#�몚&�븤+�삊/�뿋2�땲.�QO �-/�(�5e�3c�1]�$Kv�F뗎�c�9x�V뤝�2f�,�)�)�+�(�)�)�(�(�)�(�)�)�(�(�)�(�)�(�(�(�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�,�,�+�,�(�(�(�(�*�(�)�)�(�6o�U먒�K띛�R붨�0v�<e�1_�3c�4e� $�13�XU�뱪4�뵍1�뵊-�릧'�똽#�뒃�����똼�뛼�똻��~�뛻�븞�젒�캕�講E�瑃�瑃�瑃�瑃�瑃�엮�瑃�瑃�瑃�瑃�瑃�회:�츦�윇�뵁�럞�뛽�룄�똼�땫�땬�땫�뛽#�릧'�뵊.�뵍1�뱪4�VS�02�#�4e�3c�1_�>f�C댭�b�A~�\뽗�6p�,�)�)�+�(�)�)�(�(�)�(�(�(�(�(�)�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �,�*�*�,�,�,�*�*�,�,�*�*�*�*�*�*�(�)�)�(�=y�\믅�8a�G{�5z�8^�1`�3d�4e�!�36�[V�뼋6�뭾/�뭽+�뛿$�뒃 �����럟�릢�뢾�뢿�뱟�뿁�죸�츧�챰,�音�涕�涕�涕�涕�涕�쑵�涕�涕�涕�涕�涕�嵬�윗#�췀�젒�뿁�몖�몖�뭷�룄�뛼�뛼�똽 �럣$�뱦+�뭾.�뼋6�ZU�25� �4e�3d�1`�8_�B눺�+e�?n�`쇭�;y�,�)�)�+�(�)�)�(�(�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�(�)�,�,�,�(�(�(�(�(�(�(�(�(�*�(�)�)�(�F�%>Z�,D�*@�7o�7\�1a�3d�4f��47�\W�솘8�릩.�뜃(�땬#��~��~�럞�봿�뻽�솆�쓮�젏�찙�천K�裴�適�蒼�蒼�蒼�蒼�蒼�蒼�쉠�蒼�蒼�蒼�蒼�蒼�蒼�靷�隷s�쉰<�쪜�윇�썙�삃�뿁�뱡�룂�똼�뛾#�럥(�릩-�삎7�\W�36��4f�3d�1a�7\�@y�)@�(>�0Mn� F�,�)�)�+�(�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �(�(�(�*�,�,�(�(�(�(�(�(�(�(�(�*�(�)�)�(�H�8U� <\�6T�7h�8\�1a�3d�4f��47�\W�뼌6�럧.�뒆'�� ��~�땫�뱡�쐩�짅�랬9�睾o�횔W�퓔H�桎�桎�桎�桎�桎�桎�桎�桎�桎�섄�桎�桎�桎�桎�桎�桎�桎�桎�桎�획M�캘N�횝a�괌+�짃�쓮�븞�뛽�땬 �뜂&�럧-�뼌6�\W�36��4f�3d�1a�;`�2b�5R�:Y�5P�I�,�)�)�+�(�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �(�(�(�*�(�)�)�)�)�)�)�(�(�(�(�*�(�)�)�(� @|�Jy�*Mt�7^�<}�:`�1`�3d�4f��47�[V�뱪4�뜆.�늹&��~ ��}�릢�변[�凜�飮�卽�卽�芋�섄�詣�卽�卽�卽�卽�卽�卽�卽�兪�빻�音�卽�卽�卽�卽�卽�卽�卽�芋�섄�詣�卽�卽�譽�錟�땝Q�뭷�똽�땯%�럦-�뱪4�[V�36��4f�3d�1`�=c�5v�%Gl�#Be�Bl� A}�,�)�)�+�(�)�)�(�(�)�(�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�(�(�*�(�)�)�)�)�)�)�)�)�(�(�*�(�)�)�(�8q�Y볂�I뗉�Y싖�:|�<d�1`�3d�4f��35�XS�럨2�뙂.�뀅&��}��}�몖�搏�鏃�鏃�鏃�鏃�鏃�鋼P�僅`�鏃�鏃�鏃�鏃�鏃�鏃�鏃�翔�벨�裳�鏃�鏃�鏃�鏃�鏃�鏃�鏃�瑙f�跏J�鏃�鏃�鏃�鏃�鏃�輩�뵂�땭�뎱%�뜄.�럨2�XS�24��4f�3d�1`�@g�.t�/m�V먒�]붢�7q�)�)�)�+�(�)�)�(�(�)�(�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�(�(�*�(�)�)�(�(�(�(�(�(�(�(�*�(�)�)�(�2f�R듔�@궹�J뚏�7y�Eq�2_�3d�4f� �.0�RN �뀇,�뙃/��&��|��|�릢�쉼Q�足�足�足�足�足�續�섐�掌�足�足�足�足�足�足�潔V�벨�适^�足�足�足�足�足�足�掌�섐�鮮�足�足�足�足�足�엿W�뵃�땭�늺%�뜆.�뀇,�RN �.0� �4f�3d�2_�"Is�$j�'d�T뫱�R듔�2e�.�)�)�+�(�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �*�*�*�*�(�)�)�(�(�*�*�*�*�*�*�*�(�)�)�(� 0[�H�A꺢�F됎�4v�"N��3]�4d�5f�%-�*+�KJ �{x&�뙄0��&�z�굖�뛻�쨼�芋�珽�珽�珽�珽�珽�졈3�率z�珽�珽�珽�珽�珽�庵�츩�쮻�캗�縯�珽�珽�珽�珽�珽�妄��졀.�珽�珽�珽�珽�珽�兪�쬀�몘�늸�뇗%�뜇/�{x&�KJ �*+�%-�5f�4d�3]�)V�d�,j�W붣�J�� /Z�,�)�)�+�(�)�)�(�(�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �(�(�(�(�(�)�)�(�(�(�(�(�(�(�(�*�(�)�)�(�+L�<r�C곀�H됌�8x�&V�4[�4d�Line truncated
�mi�럪2��(�|x!�~v��~�쐩�狼y�迹�迹�迹�迹�迹�妄�열*�杖�迹�迹�暲�琅�쭥�븞�몗�뿁�첍$�撈�杖�迹�迹�杖�유.�勞~�迹�迹�迹�迹�迹�率|�윉�똽�� �뀆(�럪1�mi�CD
�"#�*B�5e�4d�4[�*^�a�2o�Y붣�=r�*I�,�)�)�+�(�)�)�(�(�)�(�(�(�(�(�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �(�(�)�)�)�)�)�(�(�(�(�(�(�(�(�*�(�)�)�(�&8�(^�<y�3w�1t�X�5Y�3d�5f�3_��<>�_Z�룑4�꼥*�{w!�zt�굕�뱠�랫/�迪�迪�迪�迪�迪�音�빻�僕�迪�拍�츫,�삂���걐�}r�걐���쉳�뇹7�不�迪�僕�빻�音�迪�迪�迪�迪�迪�릇2�뿁��~��} �냳)�룑3�_Z�<>��3]�5f�3d�5Y�,i�X�=z�[뽘�*`�$5�,�)�)�+�(�)�)�(�(�)�(�(�(�(�(�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�(�(�*�(�)�)�(�%"�G�=x�/r�6{�"b�7W�4c�6f�7g��35�QN ��*�뇚.�}y$�wr�{t��~�옄�翔�暲�暲�暲�暲�暲�勤f�랬7�꺄@�뱠��}�t�um�ng�le�of�wn�걐��~�뵂�랴I�묵;�苟e�暲�暲�暲�暲�暲�鐥�젔�똼�굗��|#�늼-��+�RN �35��7g�6f�4c�7W�+p�`�M뗆�^샰�H�#�,�)�)�+�(�' �)�(�(�)�(�)�)�(�(�)�(�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�-�/�,�)�)�)�-�-�)�)�(�(�*�(�)�)�(�+�6o�C|�0q�7{�&f� Ix�5`�5f�6h�&;�$%�EE
�mg�뙅2�}'�wt�vq��w�릢�윱U�鱗�鱗�鱗�鱗�語�릎L�몗��{�|r�sk�le�f` �b\ �`[ �b\ �fa �mf �tk�|s��{�뭶�벽M�語�鱗�鱗�鱗�鱗�졔V�뱡��}�z��&�뙅1�mg�EF
�$&�&:�6h�5f�5`�)Q~�c�(e�P뚈�V롃�5n�)�,�)�)�+�(�/�' �'�(�)�(�)�)�(�(�)�(�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�)�,�/�/�,�(�(�(�/�/�-�(�(�(�*�(�)�)�(�+�.S�:p�7w�E끤�>z�(^�:_�6f�7h�5a��;>�]V�늽/�긾-�xv#�to�yq�긻�뿂�裔�靷�裔�탁b�썚��|�zq�qi�ha�b\ �_Y �[W �YT �XT �YU �\W �_Y �c] �ic�qi�{q��|�쐩�틥d�裔�靷�裔�솈��~�x�~z"�꼦,�늽/�]V�;>��5a�7h�6f�:_�;q�(g�>|�Y붢�Cx�+P�)�,�)�)�+�(�-�/�' �'�)�(�)�)�(�(�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�-�/�/�,�*�*�-�-�/�-�,�*�*�*�*�(�)�)�(�+�!%3�!W�E�"<Z�%:�#Fl�'Dg�6f�7h�8i� ,�*,�KJ �ni�뎶1�}{'�ur�sn�wp�굓�옅q�융n�뻿��~�u�rj�h` �`[ �[V �WR �VQ �UQ �TP �TP �TP �UQ �VQ �XS �[V �`[ �ha �rj��u��~�뿃�윷n�윷r��{�}v�{x��}&�뎶0�nj�KJ �*,� ,�8i�7h�6f�(Ei�'Jo�'=�+Jl�`쇪�#X� .�)�,�)�)�+�(�-�-�.�' �(�(�)�)�(�(�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�/�/�/�/�(�/�0�/�-�,�(�(�(�(�*�(�)�/�/�+�-�>��Cq�1L�7U�/H�+[�8a�7h�8j�1Y��=?�UP�긽-�긾,�xu#�qo�pk�rl�vo�yo�vn�qi �ic �a[ �ZV �VR
�SO
�QN
�QN �QN �QN �QN �QN �QN �QN �QN
�SO
�VR
�ZV �a[ �jd �rj�xp�{r�zs�xq�xs�|y"�뀇+�긽,�UP�=?��1Y�8j�7h�9b�6a�/H�6S�/I�P겢�?��'�)�,�)�)�+�(�,�,�,�.�' �'�)�)�(�(�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�,�/�-�-�,�2!�2!�/�-�(�(�(�(�(�*�(�/�2!�0�+�.�,L�@r�$Bd�#Bd�+Mr�-k�;]�7g�8j�9l�!2�$%�IH �d^�뒌2�})�us �mj�if�ib�fa�c^ �^Z �[V
�UQ
�QM �PL �NK �NK
�NK
�NK
�NK
�NK
�NK
�NK
�NK
�NK
�NK �PL �QM �UR
�[V �`[ �f`�jd�nh�qn�wu ��(�땳1�d^�IH �$%�!2�9l�8j�7g�;]�8t�#Ac�#Bd�%Bd�Cu�/N�(�)�,�)�)�+�(�+�+�+�,�.�' �(�)�(�(�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�-�/�/�/�0�2!�2!�/�-�)�)�)�(�(�*�,�2!�/�,�+�.�"�J�L꽶�K겤�X붠�=|�M�9b�9j�:k�8g� �78�PM�oj�뇞0�|z(�po �if�b_�]Z�WU�TQ �PM
�NK
�KI �KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�KI
�LJ
�OL
�RO �VT �\Y�c_�ig�rp�|z'�늾0�oj�PM�78� �8f�:k�9j�9b�$S�V�@x�J{�]믃�L�&#%�(�)�,�)�)�+�(�*�*�*�+�+�.�' �(�(�(�)�(�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �/�/�0�2!�2!�0�2!�2!�/�,�)�)�)�,�/�2!�/�/�,�(�+�.�(�-Q�<p�J녓�T뫯�N뗅�*h�<_�8i�9l�:m�'B��>A �QO�wu&�뀉/�xx'�nm�db�\Z�WU�QO �NL �LJ �KI
�JH
�IG �IG �IG �IG �IG �IG �IG �IG �IG �IG �JH
�KI
�LJ �NL �QO �VT�\[�db�nl�yx&�뀉/�wu%�QO�>A ��'B�:m�9l�8i�<_�V�(e�O뗅�]뽔�Dx�/T�+�(�)�,�)�)�+�(�*�0�*�*�+�+�.�' �'�(�)�(�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �/�/�2!�2!�2!�0�/�/�,�(�(�(�-�0�2!�2!�2!�/�)�(�+�.�-�$",�G�G궮�M뚈�M띖�J댥�%U�<d�9l� :m� ;n� 3��DF
�]V�zw'�꼧0�ww(�ll!�cc�[[�VU�QP�NM �KJ �JI
�IH
�HG �GF �GF�FE�FE�GF�GF �HG �IH
�JI
�KJ �NM �QP�VU�[[�cc�ll!�ww(�꼧0�{x'�]V�DF
�� 3� ;n� :m�9l�<d�K� \�H녏�U뤝�\빺�H�"#+�+�(�)�,�)�)�+�(�' �*�*�*�*�*�*�-�' �'�)�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �0�2!�0�2!�2!�0�0�/�,�/�/�/�0�0�0�0�/�,�)�(�-�-�/�-�,P�5j�>|�N롇�R먎�G눯�$R�"?l� <n�!<n�!<o�/� !�GH �]V�vt%�뀉1�yy)�nn#�ee�^^�XX�TS�PO�NM�KK �JI �IH �IH �HG �HG �IH �IH �JI �KK �NM�PO�TS�XX�^^�ee�nn#�yy)�뀉1�vt%�]V�HH � !�/�!<o�!<n�!<n�#@l�%R�V�>{�O뗃�Y믇�;o�-Q�*�+�(�)�,�)�)�+�(�)�/�)�/�)�)�*�*�-�' �(�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �2!�2!�0�0�0�0�/�/�/�/�0�2!�2!�0�0�/�(�)�-�0�0�/�-�,�&$�>�D{�@}�T볁�U롁�5_�.Ms�'Ct�!<o�"<o�=c�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=c�"<o�"<o�)Eu�-Lq�&O~�>{�F꺠�M댥�U뚂�>�$"$�*�+�(�)�,�)�)�+�(�' �)�' �)�)�)�)�)�*�-�' �'�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �0�0�0�0�0�0�/�0�2!�0�2!�2!�2!�/�0�0�-�-�0�0�/�/�,�/�,�(D�$Y�B~�D궴�":V�*A�+A�-Ou�$?o�<f�Ei�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Ei�<f�&Ar�-Lp�)?�*@�+Jl�P뗃�U뤝�#X�+H�*�*�+�(�)�,�)�)�+�(�)�/�)�/�)�/�)�)�)�)�-�' �'�(�(�(�)� �뇦� �MNN�MNN� �뇦� �0�0�0�/�/�/�2!�2!�2!�0�0�2!�0�/�0�0�0�0�0�/�/�/�/�/�,�)�-U�Au�C|�4O�;[�5R�3\�#Em�>\�U�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�U�>\�%Hq�&Iq�7T�:Z� ;X�M됇�<p�/U�*�*�*�+�(�)�,�)�)�+�(�& �(�& �(�& �(�(�(�(�)�)�-�' �(�(�(�)� �뇦� �MNN�MNN� �뇦� �0�0�2!�2!�2!�0�0�2!�2!�2!�2!�2!�2!�2!�2!�2!�0�0�0�!�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�.E�Kr�Y�Y�Mz� 0N� 0O� 0O� 0O� 0O� 0O� 0O� 0N� /N� /N� /N� 0O� 0O� 0O� 0O� 0O� 1P� 0O� 0O� 0O� 0O� 0O� 1O� 0O� 0O� 0N� 0N� 0O� 0O� 0O� 0O� 0O�N{�Y�Y�Kr�.E�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)��)�+�(�(�(�(�/�(�/�(�/�(�(�(�(�,�' �'�(�(� �뇦� �MNN�MNN� �뇦� �0�0�2!�2!�2!�0�0�0�2!�0�2!�2!�2!�2!�2!�0�0�/� �7P�#V}�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�Mp�Aa�X�Y�T� /L�5T�8Y�9Z�9[�:[�:\�;]�:\�:]�:]�9[�9[�;]�;^�<^�;^�;^�<^�<`�=a�=a�=`�<`�<`�=`�<`�<`�<_�;^�<_�=a�:\�3T�T�Y�X�Aa�Mp�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�!Sy�#V}�7P��*�(�(�(�& �(�& �(�& �(�(�(�(�(�(�,�' �(�(� �뇦� �MNN�MNN� �뇦� �2!�2!�2!�2!�0�/�0�0�/�/�2!�2!�0�0�0�/�-� �/F�!Pv�Hk�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�@_�Q|�Y�X�7Y� 5V�;]�;]�:]�;^�Fi�j뜲�;]�<^�<_�<`�<`�<_�<^�<_�<`�<_�;^�<^�<_�<_�<_�;_��돪�;^��<^�;^�;^�;^�;]�;]�<^�;]� 5V�9[�X�Y�Q|�@_�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Fi�Hk�!Pv�/F��(�(�(�(�(�)�(�(�(�(�(�(�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �0�0�0�0�0�/�/�-�-�-�0�0�0�0�0�0��)=�!Pu�Gk�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Ab�Ei�Y�Y�Ck�6V�=a�<`�;^�;^�;]�i떘�▤�9[�i떘�▤�▤�▤�▤�▤�▤�▤�Eh�:\�눏�눏�눏�눏�렡�▤�렡�눏�눏�눏�눏�;]�<^�;^�:[�9[� 4S�Ai�Y�Y�Ei�Ab�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Eh�Gk�!Pu�)=��)�)�)�)�*�)�)�)�)�)�)�)�)�)�)�)�*� �뇦� �MNN�MNN� �뇦� �0�0�0�,�/�/�-�-�/�0�2!�2!�0�2!�0��$6�!Nr�Gk�Dg�Dg�8T�/F�/F�/F�/F�/F�/F�/F�/F�0H�>\�U�Y�My� 3R�9[�9[�:\�;]�;^�9\}�v샍�읚�Ce�g닼�떙�Cb�Cc�Cc�Cc�Cc�읚�Ce� 7V�g닼�g닶�g닼�g닶�v샋�읚�v샋�g닶�g닶�g닶�g닼�7X�9Z�9[�9[�9[�9[� 2Q�Lx�Y�U�>\�0H�/F�/F�/F�/F�/F�/F�/F�/F�/F�8T�Dg�Dg�Gk�!Nr�$6�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)� �뇦� �MNN�MNN� �뇦� �/�/�,�/�-�/�0�0�0�2!�2!�2!�0�0�0� 1�!Ko�Fj�Be�Be�7S�Hh�/r�.o�.o�.o�-m�.p�.p�-l� Q}�Kr�Y�U�2R�9[�<^�;^�;]�;]�;^�겈�쎙�쎙�쎙�Ac�Tu�s빓�s빖�+Nm�렢�]}�d뀰�,Po�Bd�d넦�d넢�d넦�d넦�s빓�쎙�s빓�d넦�d넦�d넢�d넧�,Rt�=a�=a�=a�=a�>a�;\�4T�T�Y�Ks�"Z~�4겑�5~�5~�5z�2w�1y�3~�3}�2z�Mh�7S�Be�Be�Fj�!Ko�)�)�(�(�(�(�)�)�)�)�)�)�)�)�(�(�*� �뇦� �MNN�MNN� �뇦� �-�-�,�-�/�0�0�0�0�0�2!�2!�0�/�-� Hj�Fi�Ad�Ac�9V�8S�-m�.r�-o�-l�-l�-m�.p�.o�&_�Aa�X�X�9\� 6W�;^�<_�;^�;]�:\�9Z�6Xw�p뫑�뿻�?_}�*Kj�~졗�뫒�Rs�@a��Z{�뿻�w쇊�Bc�b깶�뿻�~졗�~졗�꽘�뿻�뿻�~졗�~졗�~졗�꽘�뿻�@a�9Y�9Z�9[�:\�9[�9[� 5U�8Y�X�X�Aa�"T�(_�)a�)a�)c�*g�+g�+h�+i�)b�7T�9V�Ac�Ad�Fi�)�)�(�(�*�(�(�(�(�(�(�(�(�(�(�(�*� �뇦� �MNN�MNN� �뇦� �0�,�-�-�0�0�2!�2!�2!�0�0�0�/�0�/� Fi�@b�?a�;[�.G�#S}�&Z�%U�$U�%V�%V�%X�%X�$U��De�Q|�Y�@f� 1O�8Y�8Y�8X�8Y�8Y�7X�6V� 3Q�_~�벜� 1P�?^�{쑠�4So�_�벜� 1O�>]z�f눉� 4R�_�벜�)Gd�_~�뜳�m렜�)Gc�4So�{쑠�Xw�>]z�벜�>^}�6W�7W�7W�7Y�7X�8Y�7X� 1O�?f�Y�Q|�Cf�#S��#S�$U�&Y�&Z�%V�&X�&X�%V�!Nz�.G�;[�?a�@b�)�)�(�(�*�*�*�*�*�*�*�*�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �0�,�-�/�/�0�0�2!�2!�2!�0�0�0�0�0�?a�>`�>`�3N�'`�1w�1w�0r�0r�1t�/r�.p�0u�1w�Nr�Q|�Y�Cl�7W�=a�=a�=a�<`�<_�<_�<`�4Wy�x쎗�맭�꽙�d넧�x쎗�x쎗�꽙�맭�x쎗�x쎗�x쎗�Np�7X� 5U�x쎗�맭�맭�맭�맭�맭�r뵸�3Vv� 7W� 7V�9[�=`�>b�>b�=a�=a�>a�=a�7W�Dm�Y�Q|�Pr�3z�3y�3z�4}�4}�3y�3y�3z�5겑�2y�'`�3N�>`�>`�)�)�(�(�*�(�(�(�(�(�(�(�(�(�(�(�*� �뇦� �MNN�MNN� �뇦� �,�/�,�,�-�/�0�2!�0�2!�2!�/�/�/�/�?a�=_�=_�4P�#Su�3z�1w�1u�1u�/t�0u�0u�1w�0t�'`�Aa�X�X�9[�7Y�<`�<_�<_�<_�<^�:]�v샍�뜳�뜳�b깵�Mn�[|�b깶�뜳�뜳�|왒�[|�[|�<_~�9Z�8Y�A`�Ml�|왏�궏�[|�?\�[|�눏�3Ww�:[�;]�<_�<_�<^�<^�<_�<_�7Y�8]�X�X�Aa�)e�1w�1w�/r�/t�1u�/r�.o�/r�/t�/r�"Ru�4P�=_�=_�)�)�(�(�*�(�(�(�(�(�(�(�(�(�(�(�*� �뇦� �MNN�MNN� �뇦� �,�0�/�0�/�/�-�/�0�0�0�0�-�,�,�!Fk�=`�;^�9[�1N�Gu�*b�+d�,h�-k�+g�)_�)]�)c�)b�Ms�Ks�Y�S� 1O�7W�8Z�8Z�8Y�8Y�7X�2Rp�a��떘� 1M� 3R�=[�z쎖�떘�떘�떘�1Qn� 2O� 4R�7V�St�떘�떘�떘�t빒�t빒�t빓�z쎗�떘�떘�Ll�8Y�8Y�8Y�8Y�8X�7W�6V� 0N�S�Y�Ks�Ls�)]�)_�)_�)`�)`�)`�(_�(]�)_�)a�Hu�1N�9[�;^�=`�)�)�(�(�*�(�)�)�)�)�)�)�)�(�(�(�*� �뇦� �MNN�MNN� �뇦� �/�0�0�0�0�/�-�-�/�/�/�/�/�/�-�:U�"Ej�<_�:]�9Z�1M�"Pt�/p�.m�-l�.m�.o�/p�/p�,i�(`�>\�U�Y�N{�3S�;^�;^�;]�;^Line truncated
*E�
)D�
)D� +H� ,I� ,J� ,I� +G� *E� +G� ,H� ,H� +H� +G�
*F� )E� )E�
(C�
(B� )D� *F� ,H� +I� *F�
*E� +G� ,J� ,I� ,I�Ju�Y�Y�Ks�Ac�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�%Gl�'Jq�9T� �'�(�)�)�(�(�*�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�,�,�,�,�,�-�-�-�,�,�,�(��
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)� -�>\�U�Y�Y�Y�Y�Y�X�X�X�Y�Y�Y�Y�X�X�Y�Y�Y�Y�Y�Y�Y�Y�X�W�X�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�U�>\� .�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)�
)��%�+�(�)�)�(�(�*�(�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�,�+�.�(�)�,�(�)�+�)�(�*�*�(�(� ,5�Ei�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Y�Ei�+5�(�(�+�*�(�*�*�*�(�(�+�+�(�(�,�)�)�.�(�)�)�(�(�*�(�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �,�,�,�,�,�,�,�,�,�,�,�,�,�,�(�*�(�)�)�(�+�.�(�)�,�(�)�)�,�)�(�*�(�(�*�/>�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�=[�.=�(�(�*�+�(�(�*�*�*�(�)�)�)�(�)�,�)�)�.�(�)�)�(�(�*�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�,�*�*�*�,�,�,�,�,�,�,�,�,�,�,�(�)�)�(�+�.�(�)�,�(�)�)�,�)�(�(�*�(�(�*�+�+�*�*�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�*�+�+�+�*�*�(�*�*�*�+�(�)�)�)�(�)�,�)�)�.�(�)�)�(�(�(�*�*�*�*�*�*�*�*�*�*�*�*� �뇦� �MNN�MNN� �뇦� �-�*�,�,�,�,�,�,�,�(�(�(�(�,�,�,�,�)�)�(�+�.�(�)�,�(�(�)�+�)�(�+�(�(�(�(�*�*�*�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�*�*�*�*�(�(�*�*�*�+�(�)�)�(�(�)�,�)�)�.�(�)�)�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �(�*�*�,�,�,�,�,�,�,�(�,�,�,�(�(�)�)�)�(�+�.�(�)�,�(�(�)�)�)�(�*�+�*�(�(�(�(�(�(�(�(�(�(�(�*�*�*�*�*�*�(�(�(�(�(�(�(�(�*�*�*�*�*�*�(�(�(�(�(�(�(�(�(�(�(�(�*�*�*�*�*�(�)�)�(�(�)�,�)�)�+�(�)�)�)�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �(�(�*�*�,�,�,�,�,�,�,�,�-�-�-�)�)�)�(�(�+�.�(�)�,�(�)�(�)�+�)�(�(�(�*�(�(�(�(�(�(�(�(�*�*�*�*�*�*�*�*�*�(�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�(�(�(�(�(�(�*�*�*�(�(�(�(�)�(�(�)�(�)�,�)�)�+�(�(�)�)�)�)�)�(�(�)�)�(�(�(�(�(�)�)� �뇦� �MNN�MNN� �뇦� �(�(�(�(�(�*�,�,�,�,�,�,�,�-�-�,�)�(�(�+�.�.�(�)�,�(�(�)�+�)�+�)�)�)�)�(�*�*�*�*�*�(�*�*�*�*�*�(�*�(�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�(�(�)�)�(�)�(�)�+�(�(�)�,�)�)�+�.�(�(�)�)�)�)�)�+�)�)�)�(�(�)�)�(�(� �뇦� �MNN�MNN� �뇦� �(�(�(�(�(�(�(�*�,�,�,�,�,�,�,�(�(�(�+�.�)�+�(�)�,�(�)�(�(�+�(�)�+�+�,�)�(�*�*�*�*�(�*�*�*�*�+�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�(�*�*�*�*�*�*�*�*�(�(�+�+�+�)�(�)�(�(�+�(�)�,�)�(�)�+�.�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(� �뇦� �MNN�MNN� �뇦� �,�*�*�*�*�*�*�*�*�*�*�*�,�+�+�+�*�+�+�)�+�.�(�(�,�(�)�(�(�)�(�)�)�)�)�,�)�(�*�*�(�(�(�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�.!�+�*�*�*�*�(�(�*�(�(�)�)�)�)�)�(�)�(�(�)�(�)�,�)�)�+�)�+�+�+�+�+�.�.�.�+�+�+�+�+�+�+�+�+� �뇦� �MNN�MNN� �뇦� �(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�)�(�)�+�(�(�,�(�(�)�)�(�)�(�(�)�)�+�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�)�)�(�(�)�(�)�)�(�(�)�)�)�(�)�(�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�(�(�(�(�(�(�(�(�(�(�(�(�(�(�)�)�)�)�)�(�(�)�(�(�(�(�(�(�(�(�(�(�(�(�)�)�)Line truncated
�x릝�v�J�b�9�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�^�6�b�9�v�J�q늮�
�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��놃X�`�4�\�1�\�1�\�1�\�1�\�1�\�1�A{"�.W�A{"�\�1�\�1�A{"�.W�A{"�.W�A{"�\�1�\�1�\�1�\�1�A{"�.W�A{"�\�1�\�1�\�1�\�1�\�1�\�1�\�1�A{"�.W�A{"�A{"�.W�.W�.W�.W�.W�A{"�\�1�\�1�\�1�\�1�A{"�.W�.W�A{"�A{"�.W�A{"�\�1�\�1�\�1�\�1�\�1�`�4�놃X��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)����F�[�+�[�+�[�+�[�+�[�+�[�+�[�+�-S�汎��-S�[�+�[�+�-S�汎��-S�汎��-S�@v�[�+�[�+�[�+�-S�汎��-S�[�+�[�+�[�+�[�+�[�+�[�+�[�+�-S�汎��-S�-S�汎��汎��汎��汎��汎��-S�[�+�@v�-S�-S�-S�汎��汎��-S�-S�汎��-S�[�+�[�+�[�+�[�+�[�+�[�+���F��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��{�=�Y�&�Y�&�Y�&�Y�&�Y�&�Y�&�Y�&�,O�藜�,O�,O�?p�,O�藜�,O�,O�藜�,O�Y�&�Y�&�Y�&�,O�藜�,O�Y�&�Y�&�Y�&�Y�&�Y�&�Y�&�Y�&�,O�藜�,O�,O�藜�,O�,O�,O�藜�,O�Y�&�,O�藜�藜�藜�,O�,O�?p�,O�藜�,O�Y�&�Y�&�Y�&�Y�&�Y�&�Y�&�{�=��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��y�3�W� �W� �W� �W� �W� �W� �W� �+K�橋�橋�橋�+K�+K�橋�+K�+K�+K�=j�+K�+K�+K�+K�橋�+K�+K�+K�+K�+K�=j�=j�+K�+K�橋�+K�+K�橋�橋�橋�橋�橋�+K�W� �=j�+K�+K�橋�+K�W� �W� �+K�橋�+K�W� �W� �W� �W� �W� �W� �y�3��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��v�+�U��U��U��U��U��U��U��*G �톈�*G �*G �*G �*G �톈�톈�톈�톈�*G �톈�톈�톈�톈�톈�톈�톈�톈�톈�톈�*G �*G �톈�톈�톈�톈�톈�톈�*G �*G �*G �톈�*G �U��U��U��*G �톈�*G �U��U��*G �톈�*G �U��U��U��U��U��U��v�+��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��s�#�S��S��S��S��S��:_�)C �)C �쁨�)C �)C �쁨�쁨�쁨�)C �)C �)C �:_�)C �)C �)C �)C �쁨�)C �)C �)C �)C �쁨�)C �:_�)C �)C �쁨�)C �)C �쁨�쁨�쁨�쁨�쁨�)C �:_�)C �)C �)C �쁨�)C �)C �)C �)C �쁨�)C �)C �:_�S��S��S��S��s�#��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��g��Jr �Jr �Jr �Jr �Jr �%9�겔�겔�겔�겔�겔�%9�%9�겔�%9�겔�%9�Jr �Jr �Jr �Jr �%9�겔�%9�Jr �Jr �%9�겔�%9�Jr �Jr �%9�겔�%9�4P �%9�%9�%9�%9�%9�%9�%9�겔�겔�겔�겔�겔�겔�겔�겔�겔�겔�겔�%9�Jr �Jr �Jr �Jr �g���)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��=n�,E�,E�,E�,E�,E�"�Ξ�"�"�"�Ξ�"�"�Ξ�"�Ξ�"�,E�,E�,E�0�"�Ξ�"�,E�,E�"�Ξ�"�0�"�"�Ξ�Ξ�"�Ξ�Ξ�Ξ�Ξ�Ξ�Ξ�"�"�"�"�Ξ�"�"�"�"�Ξ�"�"�0�,E�,E�,E�,E�=n��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��>s�-H�-H�-H�-H�-H�$�싄�$�-H�$�싄�$�$�싄�$�싄�$�-H�-H�-H�$�싄�$�3�-H�-H�$�싄�$�$�싄�싄�싄�$�3�$�$�싄�$�$�$�3�-H�-H�$�싄�$�-H�-H�$�싄�$�-H�-H�-H�-H�-H�-H�>s��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��A{
�/M�/M�/M�/M�/M�&�뤐�&�&�&�뤐�&�!6�&�뤐�&�&�!6�/M�!6�&�뤐�&�/M�/M�/M�&�뤐�&�!6�&�&�뤐�&�&�뤐�&�뤐�뤐�뤐�&�/M�/M�!6�&�뤐�&�/M�/M�&�뤐�&�/M�/M�/M�/M�/M�/M�A{
��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��G��3S �3S �3S �3S �3S �)�끆�끆�끆�끆�끆�)�)�끆�끆�)�끆�)�$:�)�끆�)�$:�3S �3S �3S �)�끆�)�3S �3S �)�끆�)�)�끆�)�끆�)�)�$:�3S �3S �)�끆�)�$:�3S �3S �)�끆�)�3S �3S �3S �3S �3S �3S �G���)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��L��7Z �7Z �7Z �7Z �7Z �-�|닯�-�-�-�-�-�|닯�-�-�|닯�|닯�-�-�|닯�-�'@ �7Z �'@ �-�-�-�|닯�-�7Z �'@ �-�|닯�-�-�|닯�-�|닯�-�-�-�'@ �'@ �-�|닯�-�7Z �7Z �7Z �-�|닯�-�7Z �7Z �7Z �7Z �7Z �7Z �L���)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��S��;b�;b�;b�;b�;b�)E �1�)E �;b�;b�1�t��1�)E �)E �1�t��1�t��1�)E �;b�;b�1�t��t��t��1�)E �;b�1�t��t��1�t��1�t��t��t��t��t��1�1�t��1�)E �;b�;b�;b�1�t��1�;b�;b�;b�;b�;b�;b�S���)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)��Z�&�Ao�>i�>i�>i�>i�>i�>i�>i�>i�>i�,J�4
�,J�>i�>i�,J�4
�,J�4
�,J�>i�>i�>i�,J�4
�4
�4
�,J�>i�>i�,J�4
�4
�,J�4
�,J�4
�4
�4
�4
�4
�,J�,J�4
�,J�>i�>i�>i�>i�,J�4
�,J�>i�>i�>i�>i�>i�Ao�Z�&��)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �S�6�Q�!�Dt�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�An�Dt�Q�!�O�4�
�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�" ��V�;�a�/�^�*�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�]�)�^�*�a�/�V�;��$ �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�" � ��������������������������������������������������������� �" �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �( �( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦� �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)� �뇦� �MNN�MNN� �뇦�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�뇦� �MNN�MNN� �뇦�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�뇦� �MNN�MNN� �뇦�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�( �)�)�( �( �)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�뇦� �MNN�MNN� �뇦� ��������������������������������������������������������������������������������������������������������������� �뇦� �MNN�MNN� �뇦���{;�즬B�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�쓚C�즬B��{;��뇦� �MNN�MNN� �뇦��웭@�뛧@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@�긟@Line truncated
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�kI*�( �뇦� �MNN�MNN� �뇦�)�jH)�@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �jH)�)�뇦� �MNN�MNN� �뇦�)�iG(�?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �iG(�)�뇦� �MNN�MNN� �뇦�)�hF(�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �hF(�)�뇦� �MNN�MNN� �뇦�)�gE'�<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �gE'�)�뇦� �MNN�MNN� �뇦�)�fD'�;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �fD'�)�뇦� �MNN�MNN� �뇦�)�eC&�9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �eC&�)�뇦� �MNN�MNN� �뇦�)�dB&�8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �dB&�)�뇦� �MNN�MNN� �뇦�)�cA%�7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �cA%�)�뇦� �MNN�MNN� �뇦�)�b@$�6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �b@$�)�뇦� �MNN�MNN� �뇦�)�a?$�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�a?$�)�뇦� �MNN�MNN� �뇦�)�`>#�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3Line truncated
���������������������������������������������������������������������������������������������������������������
�뇦� �MNN�MNN� �뇦��쯻9�웭@�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁱A�쁰@�뾪8�엙5�쯹6��뇦� �MNN�MNN� �뇦��쉜=�냙>�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�|^=�{];��]1�쁫2��뇦� �MNN�MNN� �뇦��똦;�sW8�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�븝�fO7�fO7�븝�fO7�fO7�fO7�븝�fO7�fO7�fO7�fO7�fO7�fO7�fO7�븝�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�븝�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�븝�fO7�븝�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�fO7�lM+��`.��뇦� �MNN�MNN� �뇦��꼊7�iQ5�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�븝�[H3�븝�[H3�[H3�[H3�븝�[H3�[H3�[H3�[H3�[H3�[H3�븝�븝�븝�븝�븝�븝�븝�[H3�[H3�[H3�[H3�[H3�븝�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�븝�[H3�[H3�븝�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�[H3�aF'�Z*��뇦� �MNN�MNN� �뇦��}]4�`J/�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�븝�븝�븝�븝�븝�븝�븝�븝�R@.�R@.�븝�R@.�븝�R@.�R@.�R@.�븝�R@.�R@.�R@.�R@.�R@.�R@.�븝�R@.�R@.�R@.�R@.�븝�븝�븝�븝�R@.�R@.�븝�븝�븝�븝�븝�븝�븝�븝�븝�븝�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�R@.�W?!�wT&��뇦� �MNN�MNN� �뇦��uY1�XD-�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�븝�븝�I:*�I:*�I:*�븝�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�븝�븝�븝�I:*�I:*�I:*�I:*�I:*�븝�븝�븝�븝�븝�븝�I:*�븝�I:*�I:*�븝�I:*�I:*�븝�I:*�I:*�I:*�I:*�븝�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�I:*�N8�oO#��뇦� �MNN�MNN� �뇦��pU.�Q@)�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�븝�@5&�븝�@5&�@5&�@5&�븝�@5&�@5&�@5&�@5&�@5&�@5&�븝�븝�@5&�@5&�@5&�븝�븝�@5&�@5&�@5&�@5&�@5&�븝�@5&�@5&�븝�@5&�븝�@5&�@5&�븝�@5&�@5&�븝�@5&�@5&�@5&�@5&�븝�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�@5&�F3�iK ��뇦� �MNN�MNN� �뇦��mR.�M='�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�븝�=2%�=2%�븝�=2%�븝�븝�븝�븝�븝�=2%�=2%�븝�븝�=2%�=2%�=2%�=2%�=2%�=2%�=2%�븝�븝�=2%�=2%�=2%�븝�=2%�=2%�븝�=2%�븝�=2%�=2%�븝�=2%�=2%�븝�븝�븝�븝�=2%�븝�=2%�=2%�븝�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�B0�fH��뇦� �MNN�MNN� �뇦��mR.�M='�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�븝�=2%�>2%�>2%�>2%�>2%�>2%�=2%�=2%�븝�븝�븝�븝�븝�븝�븝�>2%�>2%�>2%�>2%�>2%�븝�>2%�>2%�븝�>2%�븝�>2%�>2%�븝�>2%�=2%�븝�=2%�=2%�븝�=2%�븝�=2%�=2%�븝�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�=2%�B0�fH��뇦� �MNN�MNN� �뇦��fH�B0�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�1$�1$�1$�1$�1$�1$�1$�1$�1$�븝�븝�븝�븝�븝�븝�븝�븝�븝�븝�븝�1%�1%�2%�븝�2%�2%�븝�2%�2%�븝�2%�2%�2%�2%�2%�븝�2%�2%�븝�2%�븝�1%�1%�븝�1%�1%�븝�1%�1%�븝�1%�븝�1$�븝�1%�1$�1$�1$�1$�1$�1$�1$�1$�1$�1$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�0$�B0�fH��뇦� �MNN�MNN� �뇦��fH�B0�0$�0$�1$�1$�1$�1$�1$�1$�1$�1$�1$�1$�1$�1$�1$�1%�1%�1%�1%�2%�2%�2%�2%�2%�2%�3&�3&�3&�4&�4&�4&�4&�4'�븝�5'�븝�5'�6'�5'�6'�6'�6'�6'�븝�븝�븝�븝�븝�븝�븝�7(�6(�6(�7(�7(�븝�6(�6(�븝�6(Line truncated
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�B'
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�wV0�0�뇦� �MNN�MNN� �뇦�.�wV0�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�A&
�A&
�@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �B'
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�D)
�wV0�1�뇦� �MNN�MNN� �뇦�0�vU0�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �A' �D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�D(
�vU0�1�뇦� �MNN�MNN� �뇦�1�vU0�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �A' �C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�vU0�1�뇦� �MNN�MNN� �뇦�0�uU0�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �@& �A' �C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�uU0�.�뇦� �MNN�MNN� �뇦�1�uT/�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �@& �C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�C(
�uT/�.�뇦� �MNN�MNN� �뇦�1�uT/�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �@& �B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�uT/�0�뇦� �MNN�MNN� �뇦�1�tS/�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�?% �?% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �>% �@& �B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�B'
�tS/�-�뇦� �MNN�MNN� �뇦�0�tS/�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�>% �>% �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �?& �A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�tS/�.�뇦� �MNN�MNN� �뇦�-�sS/�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �>% �A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�A'
�sS/�.�뇦� �MNN�MNN� �뇦�-�sR.�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �>% �A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�A&
�sR.�.�뇦� �MNN�MNN� �뇦�.�sR.�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �>% �@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�sR.�1�뇦� �MNN�MNN� �뇦�0�rQ.�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �=$ �@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�@&
�rQ.�1�뇦� �MNN�MNN� �뇦�.�rQ.�@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �=$ �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �@% �rQ.�0�뇦� �MNN�MNN� �뇦�.�qP.�?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �=$ �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �qP.�.�뇦� �MNN�MNN� �뇦�-�qP-�?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �=$ �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �?% �qP-�.�뇦� �MNN�MNN� �뇦�+�qP-�>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �<# �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �qP-�+�뇦� �MNN�MNN� �뇦�.�pO-�>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �<# �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �>$ �pO-�.�뇦� �MNN�MNN� �뇦�,�pO-�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �<# �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �pO-�&
�뇦� �MNN�MNN� �뇦�,�oN,�=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �:! �:! �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �:" �;" �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �=$ �oN,�+�뇦� �MNN�MNN� �뇦�,�oN,�=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �9! �9! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:" �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �=# �oN,�+�뇦� �MNN�MNN� �뇦�,�nM,�<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �:" �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �nM,�+�뇦� �MNN�MNN� �뇦�,�nM,�<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �:" �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �<# �nM,�+�뇦� �MNN�MNN� �뇦�)�nL,�;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �9! �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �nL,�+�뇦� �MNN�MNN� �뇦�(�mL+�;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �9! �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �mL+�+�뇦� �MNN�MNN� �뇦�*�mK+�;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �9! �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �;" �mK+�+�뇦� �MNN�MNN� �뇦�.�lK+�:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �7 �7 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �lK+�&
�뇦� �MNN�MNN� �뇦�0�lJ+�:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �8 �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �:! �lJ+�&
�뇦� �MNN�MNN� �뇦�0�kJ*�9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �8 �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �9! �kJ*�&
�뇦� �MNN�MNN� �뇦�0�kI*�9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �8 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �9 �kI*�+�뇦� �MNN�MNN� �뇦�0�jI*�8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �6 �6 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �jI*�&
�뇦� �MNN�MNN� �뇦�0�jI*�8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �7 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �8 �jI*�*�뇦� �MNN�MNN� �뇦�0�iH)�7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �7 �iH)�*�뇦� �MNN�MNN� �뇦�-�iH)�7 �7 �7 �7 �7 �7 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �6 �7 �7 �7 �7 �7 �7 �iH)�+�뇦� �MNN�MNN� �뇦�0�iG)�7 �7 �7 �7 �7 �7 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �5 �7 �7 �7 �7 �7 �7 �iG)�+�뇦� �MNN�MNN� �뇦�0�hG)�6 �6 �6 �6 �6 �6 �5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�5�6 �6 �6 �6 �6 �6 �hG)�+�뇦� �MNN�MNN� �뇦�1�hF)�6 �6 �6 �6 �6 �6 �4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�6 �6 �6 �6 �6 �6 �hF)�+�뇦� �MNN�MNN� �뇦�1�gF(�5 �5 �5 �5 �5 �5 �4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�4�5 �5 �5 �5 �5 �5 �gF(�+�뇦� �MNN�MNN� �뇦�1�gE(�5 �5 �5 �5 �5 �5 �3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�4�4�4�5 �5 �5 �gE(�+�뇦� �MNN�MNN� �뇦�1�fE(�4�4�4�)�)�)�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�����4�4�fE(�+�뇦� �MNN�MNN� �뇦�0�fD(�4�4�,�41(�41(�41(�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�궪�궪�궪�2EF�%�4�fD(�+�뇦� �MNN�MNN� �뇦�.�eD'�3�3�+�31(�31(�31(�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3�3Line truncated
��4�*�*�*�*�*�*�[9"�,!�뇦� �MNN�MNN� �뇦�,�[9"�)�)�)�)�)�)�)��8:2�Xnl�Sor�Edj�" �3�3�3�3�3�3�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�+�3�3�3�3�3�3�+� !,�@샓�Y��g��5QX��)�)�)�)�)�)�[9"�(�뇦� �MNN�MNN� �뇦�+�Z8"�)�)�)�)�)�)�41(�bqj�j�}�_xw�Sor�,1.�%�2�2�3�3�3�3�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�3�3�3�3�3�3�3� �>O�Y��r쬠�됓�궪�)�)�)�)�)�)�Z8"�,!�뇦� �MNN�MNN� �뇦�,�Z8"�)�)�)�)�)�)�31(�brk�i�}�^xw�Ror�+1-�$�2�2�3�3�3�3�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�3�3�3�3�3�3�2� �>O�Y��r쬠�됓�꽲�)�)�)�)�)�)�Z8"�(�뇦� �MNN�MNN� �뇦�+�Z8!�(�(�(�(�(�(�(��7:2�Wnl�Ror�Ddj�! �2�2�3�3�3�3�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�3�3�3�3�3�3�+� !,�@샓�Y��g��5QX��(�(�(�(�(�(�Z8!�(�뇦� �MNN�MNN� �뇦�+�Y7!�(�(�(�(�(�(�3�2�&� �4=9�Cbg�7V_�1�1�2�2�2�2�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�*�2�2�2�2�2�2�# �)��?빞�,Vf�
��2�(�(�(�(�(�(�Y7!�(�뇦� �MNN�MNN� �뇦�+�Y7!�(�(�(�(�(�(�2�2�2�0�$��-=@�0�0�2�2�2�2�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�2�2�2�2�2�2�(//�Vr�� �.�2�2�(�(�(�(�(�(�Y7!�(�뇦� �MNN�MNN� �뇦�+�Y7!�(�(�(�(�(�(�2�2�2�2�2�-�"�0�0�1�1�1�1�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�1�1�1�1�1�1�#�
�(�2�2�2�2�(�(�(�(�(�(�Y7!�(�뇦� �MNN�MNN� �뇦�+�Y6!�'�'�'�'�'�'�1�1�1�1�1�1�1�0�0�1�1�1�1�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�1�1�1�1�1�1�.�1�1�1�1�1�1�'�'�'�'�'�'�Y6!�(�뇦� �MNN�MNN� �뇦�+�X6!�'�'�'�'�'�'�1�1�1�1�1�1�1�/�/�0�0�0�0�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�)�0�0�0�0�0�0�0�1�1�1�1�1�1�'�'�'�'�'�'�X6!�(�뇦� �MNN�MNN� �뇦�+�X6 �'�'�'�'�'�'�0�0�0�0�0�0�0�/�/�0�0�0�0�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�0�0�0�0�0�0�/�0�0�0�0�0�0�'�'�'�'�'�'�X6 �(�뇦� �MNN�MNN� �뇦�+�X5 �&�&�&�&�&�&�&�&�&�&�&�&�&�'�'�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�'�&�&�&�&�&�&�&�&�&�&�&�&�X5 �(�뇦� �MNN�MNN� �뇦�+�X5 �&�&�&�&�&�&�&�&�&�&�&�&�&�'�'�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�(�'�&�&�&�&�&�&�&�&�&�&�&�&�X5 �(�뇦� �MNN�MNN� �뇦�+�W5 �&�&�&�&�&�&�&�&�&�&�&�&�&�&�&�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�&�&�&�&�&�&�&�&�&�&�&�&�&�W5 �(�뇦� �MNN�MNN� �뇦�+�W5 �&�&�&�&�&�&�&�&�&�&�&�&�&�&�&�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�&�&�&�&�&�&�&�&�&�&�&�&�&�W5 �(�뇦� �MNN�MNN� �뇦�*�W4 �&�&�&�&�&�&�&�&�&�&�&�&�&�&�&�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�&�&�&�&�&�&�&�&�&�&�&�&�&�W4 �(�뇦� �MNN�MNN� �뇦�+�W4 �%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�'�%�%�%�%�%�%�%�%�%�%�%�%�%�W4 �(�뇦� �MNN�MNN� �뇦�,�V4 �%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�%�V4 �(�뇦� �MNN�MNN� �뇦�.�`>$�.�.�.�.�.�.�.�.�.�.�.�.�.�-�-�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.�.Line truncated
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.
@@ -0,0 +1,69 @@
┤D ╟⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи Nи⌡Nи⌡Nи⌡Nи Nи Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи⌡Nи┤D ╟╗H июSФюSФюSФюRФюRФюS ФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюR ФюS ФюSФюSФюRФюSФюSФюRФюSФюRФюSФюSФюSФюSФюSФюSФюSФюSФюSФюRФюS ФюS ФюSФюRФюSФюSФюRФюS ФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюR ФюS ФюSФюSФюRФюSФюSФюSФюSФюSФюRФюRФюS ФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюSФюRФюSФюSФюSФюSФюSФюRФюSФюS ФюSФюSФюSФ╗H и╧K исV
ФсV
ФсU ФсU ФсU ФсV ФсV
ФсV
ФсV
ФсV
ФсV ФсV ФсV
ФсV
ФсV
ФсV ФсV ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV ФсV
ФсU
ФсV
ФсV
ФсV ФсV ФсV
ФсV
ФсV
ФсV ФсV ФсV ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсU ФсV ФсV
ФсV
ФсV
ФсV
ФсV ФсV ФсV
ФсV
ФсV
ФсV ФсV ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсU ФсU ФсU ФсV ФсV
ФсV
ФсV
ФсV
ФсV ФсV
ФсV
ФсV
ФсV
ФсV
ФсU
ФсV
ФсV ФсV
ФсV ФсV ФсV
ФсV
ФсV ФсV
ФсV
ФсV
ФсV
ФсV
ФсV
ФсV ФсV
ФсV
ФсV
Ф╧K ицTиъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФъaФъaФъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФъaФъaФъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФъaФъaФъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФъaФъ`Фъ`Фъ`Фъ`ФъaФъaФъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`Фъ`ФцTих`иЕoФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕoФЕoФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕoФЕoФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕoФЕoФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕoФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕoФЕoФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФЕnФх`ииoиФФФ~ФФФФФФ~ФФ~ФФ~ФФ~ФФФФ~ФФ~ФФ~ФФ~ФФ~ФФ~ФФФФФФФФФФФФ~ФФФФФФ~ФФ~ФФ~ФФ~ФФФФФФ~ФФ~ФФ~ФФ~ФФ~ФФ~ФФФФ~ФФФФФФФФФФФФФФ~ФФ~ФФ~ФФ~ФФФФ~ФФ~ФФ~ФФ~ФФ~ФФ~ФФФФФФФФФФФФ~ФФФФФФФФФФ~ФФФФФФ~ФФ~ФФ~ФФ~ФФФФФФФФФФФФ~ФФ~ФФ~ФФ~ФФФФ~ФФФФФФФФФФФФ~ФФФФФФ~ФФФФФФФФ~ФФ~ФФФФФиnии{иФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ▄ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ▄ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ▄ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█ФФ█Фи{ии┘иФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤ФФ≤Фи┘ии█иФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║ФФ║Фи█ии⌠иФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╖ФФ╖ФФ╗ФФ╗ФФ╖ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╖ФФ╖ФФ╗ФФ╖ФФ╖ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╖ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╖ФФ╖ФФ╗ФФ╗ФФ╖ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╖ФФ╖ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╗ФФ╖ФФ╗ФФ╗ФФ╗Фи⌠и╟┌╟и■ии⌠ии⌠ии■ии■ии⌠ии■ии■ии⌠ии⌠ии■ии■ии⌠ии⌠ии■ии■ии■ии■ии⌠ии■ии■ии■ии■ии■ии⌠ии■ии■ии■ии⌠ии⌠ии■ии■ии⌠ии■ии■ии■ии■ии⌠ии■ии■ии■ии■ии■ии■ии⌠ии■ии■ии⌠ии⌠ии■ии■ии⌠ии⌠ии■ии■ии■ии■ии⌠ии■ии■ии■ии■ии■ии■ии⌠ии⌠ии■ии■ии⌠ии■ии■ии⌠ии■ии⌠ии■ии⌠ии⌠ии⌠ии⌠ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■ии■и╟┌╟
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
#include "client_assets.h"
IDR_COMBAT_POWER_PANEL_SKIN_V2 RCDATA "assets\\combat_power_panel_skin_v2.bgra"
IDR_COMBAT_RANK_EXPLORE_V3 RCDATA "assets\\combat_rank_explore_v3.pbgra"
IDR_COMBAT_RANK_PIONEER_V3 RCDATA "assets\\combat_rank_pioneer_v3.pbgra"
IDR_COMBAT_RANK_FEARLESS_V3 RCDATA "assets\\combat_rank_fearless_v3.pbgra"
IDR_COMBAT_RANK_CONQUER_V3 RCDATA "assets\\combat_rank_conquer_v3.pbgra"
IDR_COMBAT_RANK_BATTLE_PINNACLE_V3 RCDATA "assets\\combat_rank_battle_pinnacle_v3.pbgra"
IDR_COMBAT_RANK_HEROIC_V3 RCDATA "assets\\combat_rank_heroic_v3.pbgra"
IDR_COMBAT_RANK_MARTIAL_MASTERY_V3 RCDATA "assets\\combat_rank_martial_mastery_v3.pbgra"
IDR_COMBAT_POWER_RANK_TOOLTIP_V2 RCDATA "assets\\combat_power_rank_tooltip_v2.bgra"
+82 -24
View File
@@ -89,8 +89,17 @@ function gmConfirm(title, message, confirmText, onConfirm) {
// 视图切换
// ============================================================
function showLauncher() {
if (window.shellGo) { window.shellGo('start'); return; }
$('launcherView').classList.remove('hidden');
$('gmView').classList.add('hidden');
const gm = $('gmView'); if (gm) gm.classList.add('hidden');
}
function showGm() {
if (window.shellGo) { window.shellGo('gm'); return; }
$('launcherView').classList.add('hidden');
const gm = $('gmView'); if (gm) gm.classList.remove('hidden');
if (!gmInited) initGm();
renderGmPage(gmCurrentPage);
}
// ============================================================
@@ -276,7 +285,7 @@ const GM_PAGES = {
inventory: ['背包装备', '/ 查看背包'],
auction: ['拍卖行', '/ 挂单上下架'],
quests: ['任务管理', '/ 任务查询'],
dungeons: ['魔改中心', '/ 副本状态'],
dungeons: ['副本控制', '/ 副本状态'],
wallet: ['钱包点券', '/ 钱包与黑钻'],
wipe: ['删档管理', '/ 删除角色与账号'],
settings: ['连接设置', '/ 数据源切换'],
@@ -433,13 +442,10 @@ function initGm() {
loadAccounts().then(() => loadCharacters());
renderGmPage(gmCurrentPage);
});
// 「返回启动器」:单窗口模式下回到启动器首页并恢复窗口
// 「返回启动器」:单窗口内切回开始页
$('btnBackLauncher').addEventListener('click', () => {
if (typeof window.shellExitGm === 'function') {
window.shellExitGm();
} else {
showLauncher();
}
if (window.shellGo) { window.shellGo('start'); return; }
showLauncher();
});
// 预加载账号/角色,供所有页面联动
loadAccounts().then(() => loadCharacters());
@@ -620,6 +626,8 @@ async function openCharDetail(charId) {
try {
const r = await api('/api/character?id=' + encodeURIComponent(charId));
const ch = r.data || {};
const fat = await api('/api/character/fatigue?id=' + encodeURIComponent(charId) + '&mode=query');
const fatInfo = fat.data || {};
content.innerHTML = `
<div class="gm-card">
<div class="gm-card-title"><span class="dot"></span>角色:${escapeHtml(ch.name || charId)}</div>
@@ -649,6 +657,23 @@ async function openCharDetail(charId) {
<div class="gm-card-title" style="margin-top:8px"><span class="dot"></span>职业 / 转职</div>
<div id="careerPanel" style="margin-bottom:10px"><div class="gm-hint">正在加载职业信息…</div></div>
<div class="gm-card-title" style="margin-top:8px"><span class="dot"></span>疲劳值(无限疲劳)</div>
<div class="gm-form-row" style="margin-bottom:10px">
<div class="gm-field">
<label>当前疲劳</label>
<input class="gm-input" id="editFatigue" type="number" value="${fmt(fatInfo.fatigue)}">
</div>
<div class="gm-field">
<label>疲劳上限</label>
<input class="gm-input" id="editFatigueLimit" type="number" value="${fmt(fatInfo.fatigue_limit)}">
</div>
</div>
<div class="gm-toolbar">
<button class="gm-btn gm-btn-sm" onclick="saveFatigue()">保存疲劳</button>
<button class="gm-btn gm-btn-sm gm-btn-primary" onclick="maxFatigue()">⚡ 一键无限疲劳</button>
<span class="gm-hint">疲劳每日 6 点重置回上限;无限疲劳 = 把疲劳和上限都设成超大值</span>
</div>
</div>
<div class="gm-card gm-danger-zone">
@@ -760,6 +785,20 @@ async function saveGold() {
if (r.ok) gmToast('金币已更新', 'ok'); else gmToast('失败:' + (r.error || ''), 'err');
}
async function saveFatigue() {
const fatigue = $('editFatigue').value;
const limit = $('editFatigueLimit').value;
const r = await post('/api/character/fatigue', { id: currentCharId, fatigue: parseInt(fatigue), fatigue_limit: parseInt(limit) });
if (r.ok) { gmToast('疲劳值已更新', 'ok'); openCharDetail(currentCharId); }
else gmToast('失败:' + (r.error || ''), 'err');
}
async function maxFatigue() {
const r = await post('/api/character/fatigue', { id: currentCharId, fatigue: 999999, fatigue_limit: 999999 });
if (r.ok) { gmToast('已设置无限疲劳(999999)', 'ok'); openCharDetail(currentCharId); }
else gmToast('失败:' + (r.error || ''), 'err');
}
async function deleteCurrentChar() {
gmConfirm('删除角色', '确定要删除该角色的全部数据吗?<br>此操作<b style="color:#ff5d6c">不可恢复</b>,且需先停止服务端。', 'DELETE', async () => {
const r = await post('/api/character/delete', { id: currentCharId, confirm: 'DELETE' });
@@ -1384,12 +1423,12 @@ async function renderAuction(content) {
<input class="gm-input" id="auction-unit-price" type="number" min="1" placeholder="单价">
</div>
<div class="gm-field">
<label>一口价</label>
<div class="gm-hint" style="padding-top:8px">本服为单一价格模式,一口价自动等于单价</div>
<label>一口价(每件,留空=单价)</label>
<input class="gm-input" id="auction-buyout" type="number" min="0" placeholder="可空">
</div>
<div class="gm-field">
<label>起拍价</label>
<div class="gm-hint" style="padding-top:8px">本服不设竞拍,按一口价(=单价)直接成交</div>
<label>起拍价(每件,留空=不设竞拍)</label>
<input class="gm-input" id="auction-start" type="number" min="0" placeholder="可空">
</div>
<div class="gm-field">
<label>挂单时长(小时)</label>
@@ -1512,20 +1551,19 @@ async function auctionList() {
const itemId = $('auction-item-id').value;
const count = parseInt($('auction-count').value || '0', 10);
const unitPrice = parseInt($('auction-unit-price').value || '0', 10);
const buyoutRaw = $('auction-buyout').value.trim();
const startRaw = $('auction-start').value.trim();
if (!seller) { gmToast('请选择卖家角色', 'err'); return; }
if (!itemId) { gmToast('请填写物品 ID', 'err'); return; }
if (!count || count <= 0) { gmToast('数量必须大于 0', 'err'); return; }
if (!unitPrice || unitPrice <= 0) { gmToast('单价必须大于 0', 'err'); return; }
// 本服拍卖行只支持「一口价 = 单价」的单一价格模式(服务端 unit_price 即唯一定价,
// 客户端只展示这一个价)。这里强制对齐:一口价=单价、起拍价=-1(buyout-only),
// 避免提交后被服务端英文校验(buyout_price must equal unit_price)拒绝。
const body = {
seller_id: seller,
item_id: parseInt(itemId, 10),
count,
unit_price: unitPrice,
start_price: -1,
buyout_price: unitPrice,
start_price: startRaw === '' ? null : parseInt(startRaw, 10),
buyout_price: buyoutRaw === '' ? null : parseInt(buyoutRaw, 10),
duration_hours: parseInt($('auction-duration').value || '24', 10),
grant_if_missing: !!$('auction-grant-missing').checked,
};
@@ -1759,7 +1797,7 @@ async function renderDungeons(content) {
<div class="gm-search-row" style="margin-bottom:10px">
<span class="gm-hint">当前爆率:</span>
<span class="gm-tag gm-tag-purple" id="hell-current">—</span>
<input class="gm-input" id="hell-input" type="number" min="0" max="15000" step="1" placeholder="百分比" style="width:140px">
<input class="gm-input" id="hell-input" type="number" min="0" max="100" step="1" placeholder="百分比" style="width:140px">
<span class="gm-hint">%</span>
<button class="gm-btn gm-btn-primary" onclick="applyHellRate()">应用</button>
<span class="gm-hint" id="hell-range"></span>
@@ -1955,7 +1993,7 @@ async function renderWallet(content) {
</div>
</div>
<button class="gm-btn gm-btn-primary" onclick="activateBlackDiamond()">💎 开启黑钻</button>
<div class="gm-hint" style="margin-top:8px">黑钻提升疲劳上限、额外翻牌等权益;在线角色回城后即时生效,无需离线。</div>
<div class="gm-hint" style="margin-top:8px">黑钻提升疲劳上限、额外翻牌等权益;角色需离线(下次登录生效)。</div>
</div>`;
fillCharSelect($('wallet-char'));
fillAccountSelect($('cera-account'));
@@ -2033,7 +2071,7 @@ async function activateBlackDiamond() {
const id = $('bd-char').value;
const days = parseInt($('bd-days').value) || 30;
if (!id) { gmToast('请选择角色', 'err'); return; }
gmConfirm('开启黑钻', '确定要为所选角色开启黑钻 ' + days + ' 天吗?<br>在线角色回城后即时生效。', '开启黑钻', async () => {
gmConfirm('开启黑钻', '确定要为所选角色开启黑钻 ' + days + ' 天吗?<br>角色需处于<b style="color:#ff5d6c">离线状态</b>,下次登录生效。', '开启黑钻', async () => {
const r = await post('/api/black-diamond/activate', { id, days });
if (r.ok) { gmToast('黑钻已开启 ' + days + ' 天', 'ok'); queryBlackDiamond(); }
else gmToast('开启失败:' + (r.error || ''), 'err');
@@ -2209,19 +2247,32 @@ function initTitlebar() {
// ============================================================
// 初始化
// ============================================================
function isGmWindow() {
// 独立 GM 窗口通过 /gm 路径打开
return window.location.pathname.endsWith('/gm');
}
function init() {
initTitlebar();
// 独立 GM 窗口:直接显示 GM 视图
if (isGmWindow()) {
showLauncher = function() {}; // 降级模式下的返回按钮已处理
$('launcherView').classList.add('hidden');
$('gmView').classList.remove('hidden');
initGm();
renderGmPage('dashboard');
refreshGmHealth();
return;
}
// 启动器按钮
$('btnStart').addEventListener('click', () => {
appendLog('▶ 开始游戏…');
action('start');
});
$('btnGm').addEventListener('click', () => {
// GM 工具现为单窗口内嵌页面,通过 shell 导航切换
if (typeof window.shellGo === 'function') {
window.shellGo('gm');
}
showGm();
});
$('btnStop').addEventListener('click', () => {
appendLog('■ 关闭服务…');
@@ -2257,6 +2308,8 @@ function init() {
window.openCharDetail = openCharDetail;
window.saveLevel = saveLevel;
window.saveGold = saveGold;
window.saveFatigue = saveFatigue;
window.maxFatigue = maxFatigue;
window.deleteCurrentChar = deleteCurrentChar;
window.doGrant = doGrant;
window.loadInventory = loadInventory;
@@ -2285,5 +2338,10 @@ window.wipeCharSelected = wipeCharSelected;
window.wipeAccountSelected = wipeAccountSelected;
window.queryWallet = queryWallet;
window.setCera = setCera;
window.applyFatigueMode = applyFatigueMode;
window.refillFatigue = refillFatigue;
window.toggleBurning = toggleBurning;
window.initGm = initGm;
window.renderGmPage = renderGmPage;
init();
+4 -2
View File
@@ -107,7 +107,9 @@ PVF_PATH = os.path.join(CLIENT_DIR, "Script.pvf")
# 一键换装工具(独立工具,放在服务端根目录的 LoadoutTool 文件夹下)
LOADOUT_DIR = os.path.join(SERVER_ROOT, "LoadoutTool")
LOADOUT_EXE = os.path.join(LOADOUT_DIR, "DNFLoadout.exe")
LOADOUT_EXE = os.path.join(LOADOUT_DIR, "换装工具.exe")
if not os.path.exists(LOADOUT_EXE):
LOADOUT_EXE = os.path.join(LOADOUT_DIR, "DNFLoadout.exe")
# 一键换装 / 服务端工具的持久化配置路径
LOADOUT_CONFIG = os.path.join(LOADOUT_DIR, "config.json")
@@ -2421,7 +2423,7 @@ def show_window(url):
# 主窗口:启动器(无边框,屏幕居中)
window = webview.create_window(
"DNF90 单机模拟器",
"90轮回",
url,
width=1204,
height=717,
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+1
View File
@@ -9,6 +9,7 @@
<AssemblyName>DNFLoadout</AssemblyName>
<RootNamespace>DNFLoadout</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\loadout.ico</ApplicationIcon>
</PropertyGroup>
</Project>
+277
View File
@@ -0,0 +1,277 @@
// Command companionoverlay — 支援兵覆盖工具(单文件版)。
//
// 把「支援兵 AIC 覆盖」做成一个可独立发布的可执行程序:内置 companionpvf 与
// profiles.json,用户只需拖入 Script.pvf(或直接双击用默认路径),即可:
//
// 1) 生成覆盖版 PVF(默认输出到同目录 Script.companion.pvf);
// 2) 可选地安装到客户端/服务端目录(自动备份、打印哈希)。
//
// 用法:
//
// 支援兵覆盖工具.exe 交互模式(使用默认客户端路径)
// 支援兵覆盖工具.exe <Script.pvf> 拖拽/命令行:只生成覆盖版
// 支援兵覆盖工具.exe <Script.pvf> --client <客户端目录> --server <服务端数据目录>
package main
import (
"bufio"
"crypto/sha256"
"embed"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
//go:embed companionpvf.exe profiles.json
var assets embed.FS
const (
appTitle = "DNF90 支援兵覆盖工具"
appVersion = "1.0.0"
defaultClient = `D:\90server\Client\Script.pvf`
minOutputBytes = 50 << 20
)
func main() {
fmt.Printf("%s v%s\n", appTitle, appVersion)
fmt.Println(strings.Repeat("=", 60))
args := os.Args[1:]
pvfPath, clientDir, serverDir, install, interactive := parseArgs(args)
if interactive {
pvfPath = prompt("要处理的 Script.pvf 路径", pvfPath)
if !ask("是否同时安装到客户端/服务端目录?(y/N): ") {
install = false
} else {
install = true
clientDir = prompt("客户端目录(含 Script.pvf)", clientDir)
serverDir = prompt("服务端数据目录(含 Script.pvf)", serverDir)
}
}
if !install {
clientDir, serverDir = "", ""
}
if strings.TrimSpace(pvfPath) == "" {
fail("没有指定 Script.pvf")
}
source, err := filepath.Abs(pvfPath)
if err != nil || !exists(source) {
fail("找不到 PVF:" + pvfPath)
}
outPath := strings.TrimSuffix(source, filepath.Ext(source)) + ".companion.pvf"
if dir := filepath.Dir(source); dir != "" {
outPath = filepath.Join(dir, "Script.companion.pvf")
}
fmt.Printf("输入 PVF:%s\n", source)
fmt.Printf("输出 PVF:%s\n", outPath)
if err := runOverlay(source, outPath); err != nil {
fail(err.Error())
}
size := fileSize(outPath)
fmt.Printf("覆盖版已生成:%d 字节(%.1f MB)\n", size, float64(size)/(1<<20))
if size < minOutputBytes {
fail("输出文件过小,疑似失败(原 PVF 未被修改)")
}
hash := fileHash(outPath)
fmt.Printf("覆盖版 SHA256:%s\n", hash)
if install {
if err := installTo(source, outPath, hash, clientDir, serverDir); err != nil {
fail(err.Error())
}
} else {
fmt.Println()
fmt.Println("未安装。手动替换方法:")
fmt.Println(" 客户端:把输出文件改名为 Script.pvf 覆盖到 客户端目录。")
fmt.Println(" 服务端:同样覆盖到 服务端 runtime\\data\\dnf\\Script.pvf。")
fmt.Println(" 完成后重启服务端,再启动游戏。")
}
fmt.Println()
fmt.Println("完成。")
if interactive {
fmt.Print("按回车退出...")
bufio.NewReader(os.Stdin).ReadString('\n')
}
}
func parseArgs(args []string) (pvf, clientDir, serverDir string, install, interactive bool) {
clientDir = `D:\90server\Client`
serverDir = `D:\90server\Server\runtime\data\dnf`
pvf = defaultClient
interactive = len(args) == 0
for i := 0; i < len(args); i++ {
switch args[i] {
case "--client":
if i+1 < len(args) {
clientDir = args[i+1]
install = true
i++
}
case "--server":
if i+1 < len(args) {
serverDir = args[i+1]
install = true
i++
}
case "--install":
install = true
default:
if !strings.HasPrefix(args[i], "--") {
pvf = args[i]
}
}
}
return pvf, clientDir, serverDir, install, interactive
}
func runOverlay(source, outPath string) error {
toolPath := filepath.Join(os.TempDir(), fmt.Sprintf("companionpvf-%d.exe", time.Now().UnixNano()))
toolBytes, err := assets.ReadFile("companionpvf.exe")
if err != nil {
return fmt.Errorf("读取内置工具失败:%w", err)
}
if err := os.WriteFile(toolPath, toolBytes, 0o700); err != nil {
return fmt.Errorf("释放内置工具失败:%w", err)
}
defer os.Remove(toolPath)
profilesPath := filepath.Join(os.TempDir(), fmt.Sprintf("companion-profiles-%d.json", time.Now().UnixNano()))
profilesBytes, err := assets.ReadFile("profiles.json")
if err != nil {
return fmt.Errorf("读取内置配置失败:%w", err)
}
if err := os.WriteFile(profilesPath, profilesBytes, 0o600); err != nil {
return fmt.Errorf("释放内置配置失败:%w", err)
}
defer os.Remove(profilesPath)
fmt.Println("生成覆盖版(约需 10–60 秒)...")
cmd := exec.Command(toolPath, "--profile-overlay", source, outPath, profilesPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("覆盖失败:%w(原 PVF 未被修改)", err)
}
return nil
}
func installTo(source, outPath, hash, clientDir, serverDir string) error {
if hasProcess("DNF") {
return fmt.Errorf("请先关闭 DNF 客户端:PVF 正被占用")
}
targets := []string{
filepath.Join(clientDir, "Script.pvf"),
filepath.Join(serverDir, "Script.pvf"),
}
stamp := time.Now().Format("20060102-150405")
for _, target := range targets {
if !exists(target) {
fmt.Printf("跳过(不存在):%s\n", target)
continue
}
if fileHash(target) == hash {
fmt.Printf("已是最新,跳过:%s\n", target)
continue
}
backup := target + ".bak_before_companion_" + stamp
if err := copyFile(target, backup); err != nil {
return fmt.Errorf("备份失败:%w", err)
}
fmt.Printf("已备份:%s\n", backup)
if err := copyFile(outPath, target); err != nil {
return fmt.Errorf("安装失败:%w", err)
}
fmt.Printf("已安装:%s\n", target)
}
fmt.Println("请重启 DNF90 服务端(服务端启动时加载 PVF),再启动游戏。")
return nil
}
func prompt(label, fallback string) string {
fmt.Printf("%s [%s]: ", label, fallback)
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
if text == "" {
return fallback
}
return strings.Trim(text, `"`)
}
func ask(label string) bool {
fmt.Print(label)
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
text = strings.ToLower(strings.TrimSpace(text))
return text == "y" || text == "yes"
}
func exists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func fileSize(path string) int64 {
info, err := os.Stat(path)
if err != nil {
return 0
}
return info.Size()
}
func fileHash(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return ""
}
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
return err
}
return out.Close()
}
func hasProcess(name string) bool {
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq "+name+".exe", "/NH")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(output)), strings.ToLower(name+".exe"))
}
func fail(message string) {
fmt.Fprintf(os.Stderr, "\n[错误] %s\n", message)
fmt.Print("按回车退出...")
bufio.NewReader(os.Stdin).ReadString('\n')
os.Exit(1)
}
@@ -0,0 +1,126 @@
package dnf90
import (
"encoding/json"
"io"
"net/http"
"strings"
appkit "longheng.io/server/internal/platform/app"
httpx "longheng.io/server/internal/platform/httpx"
"longheng.io/server/internal/services/dnfbridge"
)
// companionPanelAdminPath is the local-only endpoint used by the GM tool's
// "支援兵属性" page. It is intentionally unauthenticated: the admin mux binds
// to loopback, like the existing /local endpoints, and the page is served by
// the local GM tool.
const companionPanelAdminPath = "/local/companion-panel"
func registerCompanionPanelAdmin(env *appkit.Env, bridge *dnfbridge.Service) {
if env == nil || env.AdminMux == nil || bridge == nil {
return
}
env.AdminMux.HandleFunc(companionPanelAdminPath, func(w http.ResponseWriter, r *http.Request) {
applyCompanionPanelCORS(w)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
switch r.Method {
case http.MethodGet:
characterID := strings.TrimSpace(r.URL.Query().Get("character_id"))
if characterID == "" {
characterID = strings.TrimSpace(r.URL.Query().Get("characterId"))
}
values, found, err := bridge.CompanionPanelValues(r.Context(), characterID)
if err != nil {
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"character_id": characterID,
"found": found,
"values": values,
})
case http.MethodPost:
var body struct {
CharacterID string `json:"character_id"`
Values map[string]string `json:"values"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
if err := decoder.Decode(&body); err != nil {
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if strings.TrimSpace(body.CharacterID) == "" {
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{
"error": "character_id is required",
})
return
}
if err := bridge.SaveCompanionPanelValues(r.Context(), body.CharacterID, body.Values); err != nil {
httpx.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
values, found, _ := bridge.CompanionPanelValues(r.Context(), body.CharacterID)
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"ok": true,
"character_id": body.CharacterID,
"found": found,
"values": values,
})
default:
w.Header().Set("Allow", "GET, POST, OPTIONS")
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{
"error": "method not allowed",
})
}
})
// Base-estimate generation: PVF growth + worn equipment + weapon forging.
// Enchant cards, amplification, reinforcement, quality, sets, titles, auras
// and pets are intentionally not modelled; the GM page marks the result as
// an estimate and lets the operator correct it.
env.AdminMux.HandleFunc(companionPanelGeneratePath, func(w http.ResponseWriter, r *http.Request) {
applyCompanionPanelCORS(w)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST, OPTIONS")
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{
"error": "method not allowed",
})
return
}
var body struct {
CharacterID string `json:"character_id"`
}
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
values, err := bridge.GenerateCompanionPanelSnapshot(r.Context(), body.CharacterID)
if err != nil {
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{
"ok": true,
"character_id": body.CharacterID,
"estimate": true,
"values": values,
})
})
}
const companionPanelGeneratePath = "/local/companion-panel/generate"
func applyCompanionPanelCORS(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Admin-Token")
w.Header().Set("Access-Control-Allow-Private-Network", "true")
}
@@ -0,0 +1,234 @@
package companion
import (
"sort"
"strconv"
"strings"
)
// TeamMaxCompanions is the number of support soldiers that may enter with the
// owner. The current client's ordinary party has four slots: the owner plus
// three companions. Keeping the limit here (rather than in the bridge) makes
// every matcher, GM preflight and status projection agree.
const TeamMaxCompanions = 3
// DefaultTeamAIProfile is the fallback policy for an auto-matched teammate
// that has no per-character GM profile. Team members must actually join the
// fight, so the default is the aggressive AIC variant; operators can still
// override any character through ai_profiles (assist/aggressive).
func DefaultTeamAIProfile(classID uint16) AIProfile {
profile := DefaultAIProfile(classID)
profile.Behavior = "aggressive"
return profile
}
// TeamCandidate is the detached roster row used by the automatic matcher. It
// deliberately contains no session, inventory or mutable character pointers:
// the bridge reads one consistent repository list and passes only identity,
// level, job and online state here.
type TeamCandidate struct {
CharacterID string
Level int
Job string
Slot int
Online bool
Deleted bool
}
// TeamSkip explains why a candidate did not enter the frozen run. It is
// evidence for the GM status and packet log, not a client-facing error.
type TeamSkip struct {
CharacterID string
Reason string
}
// TeamMatch is the deterministic result of one dungeon-entry match.
type TeamMatch struct {
Selected []string
Skipped []TeamSkip
}
const (
TeamSkipOwner = "source_is_owner"
TeamSkipOnline = "source_online"
TeamSkipUnavailable = "source_unavailable"
TeamSkipLevel = "level_unsupported"
TeamSkipDeleted = "source_deleted"
TeamSkipInvalid = "source_invalid"
TeamSkipJob = "job_unsupported"
TeamSkipPartyFull = "party_full"
)
// MatchTeam forms the teammate roster for one dungeon.
//
// Rules, in order:
// 1. The GM's explicit selection is authoritative and keeps its order.
// 2. Remaining slots are filled automatically from the same account's offline,
// non-deleted characters.
// 3. Automatic picks prefer higher level first, then one character per job
// before duplicating a job, then the lower slot/id for stable output.
//
// The result is frozen by the caller. This function is pure so the selection
// cannot depend on map iteration order or on repository timing.
func MatchTeam(ownerID string, configured []string, candidates []TeamCandidate, limit int) TeamMatch {
ownerID = strings.TrimSpace(ownerID)
if limit <= 0 || limit > TeamMaxCompanions {
limit = TeamMaxCompanions
}
byID := make(map[string]TeamCandidate, len(candidates))
for _, candidate := range candidates {
id := strings.TrimSpace(candidate.CharacterID)
if !validCharacterID(id) {
continue
}
if _, exists := byID[id]; exists {
continue
}
candidate.CharacterID = id
candidate.Job = strings.TrimSpace(candidate.Job)
byID[id] = candidate
}
result := TeamMatch{
Selected: make([]string, 0, limit),
Skipped: make([]TeamSkip, 0, 4),
}
selected := make(map[string]struct{}, limit)
skip := func(id, reason string) {
if len(result.Skipped) >= 16 {
return
}
result.Skipped = append(result.Skipped, TeamSkip{CharacterID: id, Reason: reason})
}
selectID := func(id string) bool {
if _, exists := selected[id]; exists {
return true
}
if len(result.Selected) >= limit {
return false
}
selected[id] = struct{}{}
result.Selected = append(result.Selected, id)
return true
}
// Phase 1: preserve the operator's manual choice.
for _, raw := range configured {
id := strings.TrimSpace(raw)
if !validCharacterID(id) {
skip(id, TeamSkipInvalid)
continue
}
if _, exists := selected[id]; exists {
continue
}
if id == ownerID {
skip(id, TeamSkipOwner)
continue
}
candidate, found := byID[id]
if !found {
skip(id, TeamSkipUnavailable)
continue
}
if reason := teamCandidateReason(candidate); reason != "" {
skip(id, reason)
continue
}
if !selectID(id) {
skip(id, TeamSkipPartyFull)
}
}
// Phase 2: auto-fill. Sorting is made total (level, slot, numeric id) so a
// database returning rows in a different order cannot change the roster.
available := make([]TeamCandidate, 0, len(byID))
for id, candidate := range byID {
if id == ownerID {
continue
}
if _, exists := selected[id]; exists {
continue
}
if teamCandidateReason(candidate) != "" {
continue
}
available = append(available, candidate)
}
sort.Slice(available, func(i, j int) bool {
if available[i].Level != available[j].Level {
return available[i].Level > available[j].Level
}
if available[i].Slot != available[j].Slot {
return available[i].Slot < available[j].Slot
}
return teamNumericID(available[i].CharacterID) < teamNumericID(available[j].CharacterID)
})
usedJobs := make(map[string]struct{}, limit)
for id := range selected {
if candidate, ok := byID[id]; ok {
usedJobs[teamJobKey(candidate.Job)] = struct{}{}
}
}
// First pass: one character per job keeps the team varied.
for _, candidate := range available {
if len(result.Selected) >= limit {
break
}
job := teamJobKey(candidate.Job)
if job == "" {
continue
}
if _, exists := usedJobs[job]; exists {
continue
}
if selectID(candidate.CharacterID) {
usedJobs[job] = struct{}{}
}
}
// Second pass: fill the remaining slots by level/slot/id.
for _, candidate := range available {
if len(result.Selected) >= limit {
break
}
if _, exists := selected[candidate.CharacterID]; exists {
continue
}
selectID(candidate.CharacterID)
}
return result
}
func teamCandidateReason(candidate TeamCandidate) string {
if candidate.Deleted {
return TeamSkipDeleted
}
if candidate.Level < 1 || candidate.Level > 90 {
return TeamSkipLevel
}
if candidate.Online {
return TeamSkipOnline
}
if _, err := strconv.ParseUint(strings.TrimSpace(candidate.Job), 10, 8); err != nil {
return TeamSkipJob
}
return ""
}
func teamJobKey(job string) string {
parsed, err := strconv.ParseUint(strings.TrimSpace(job), 10, 8)
if err != nil {
return ""
}
return strconv.FormatUint(parsed, 10)
}
func teamNumericID(id string) int {
parsed, err := strconv.Atoi(strings.TrimSpace(id))
if err != nil {
return int(^uint(0) >> 1)
}
return parsed
}
@@ -0,0 +1,95 @@
package companion
import (
"reflect"
"testing"
)
func TestMatchTeamKeepsManualSelectionAndFillsByLevelAndJob(t *testing.T) {
candidates := []TeamCandidate{
{CharacterID: "1", Level: 90, Job: "0", Slot: 0},
{CharacterID: "2", Level: 90, Job: "12", Slot: 1},
{CharacterID: "3", Level: 90, Job: "3", Slot: 2},
{CharacterID: "4", Level: 90, Job: "0", Slot: 3},
{CharacterID: "5", Level: 21, Job: "2", Slot: 4},
}
match := MatchTeam("1", []string{"4"}, candidates, 3)
if want := []string{"4", "2", "3"}; !reflect.DeepEqual(match.Selected, want) {
t.Fatalf("selected=%v want=%v", match.Selected, want)
}
}
func TestMatchTeamSkipsOwnerOnlineDeletedLowLevelAndInvalid(t *testing.T) {
candidates := []TeamCandidate{
{CharacterID: "1", Level: 90, Job: "0", Slot: 0}, // owner
{CharacterID: "2", Level: 90, Job: "0", Slot: 1, Online: true}, // online
{CharacterID: "3", Level: 90, Job: "0", Slot: 2, Deleted: true},
{CharacterID: "4", Level: 0, Job: "0", Slot: 3},
{CharacterID: "5", Level: 90, Job: "x", Slot: 4},
{CharacterID: "0", Level: 90, Job: "0", Slot: 5},
{CharacterID: "6", Level: 90, Job: "1", Slot: 6},
}
match := MatchTeam("1", []string{"2", "3", "4", "5", "0", "99"}, candidates, 3)
if want := []string{"6"}; !reflect.DeepEqual(match.Selected, want) {
t.Fatalf("selected=%v want=%v", match.Selected, want)
}
assertSkip(t, match, "2") // online
assertSkip(t, match, "3") // deleted
assertSkip(t, match, "4") // low level
assertSkip(t, match, "5") // unsupported job
assertSkip(t, match, "99") // missing
}
func TestMatchTeamCapsAtThreeAndIsDeterministic(t *testing.T) {
candidates := []TeamCandidate{
{CharacterID: "9", Level: 90, Job: "0", Slot: 9},
{CharacterID: "7", Level: 90, Job: "2", Slot: 7},
{CharacterID: "8", Level: 90, Job: "4", Slot: 8},
{CharacterID: "6", Level: 90, Job: "1", Slot: 6},
}
first := MatchTeam("1", nil, candidates, 99)
if len(first.Selected) != TeamMaxCompanions {
t.Fatalf("selected=%v want %d entries", first.Selected, TeamMaxCompanions)
}
// Slot order wins inside the same-levelled, different-job set.
if want := []string{"6", "7", "8"}; !reflect.DeepEqual(first.Selected, want) {
t.Fatalf("selected=%v want=%v", first.Selected, want)
}
for i := 0; i < 20; i++ {
again := MatchTeam("1", nil, candidates, 99)
if !reflect.DeepEqual(again.Selected, first.Selected) {
t.Fatalf("iteration %d selected=%v want=%v", i, again.Selected, first.Selected)
}
}
}
func TestMatchTeamEmptyRosterReturnsNoSelection(t *testing.T) {
match := MatchTeam("1", []string{"2"}, nil, 3)
if len(match.Selected) != 0 {
t.Fatalf("selected=%v want empty", match.Selected)
}
assertSkip(t, match, "2")
}
func TestDefaultTeamAIProfileIsAggressiveAndJobScoped(t *testing.T) {
profile := DefaultTeamAIProfile(12)
if profile.Behavior != "aggressive" {
t.Fatalf("behavior=%q want aggressive", profile.Behavior)
}
if profile.ClassID != 12 {
t.Fatalf("class id=%d want 12", profile.ClassID)
}
if err := profile.Validate(); err != nil {
t.Fatalf("default team profile is invalid: %v", err)
}
}
func assertSkip(t *testing.T, match TeamMatch, id string) {
t.Helper()
for _, skipped := range match.Skipped {
if skipped.CharacterID == id {
return
}
}
t.Fatalf("candidate %s was not recorded as skipped: %+v", id, match.Skipped)
}
@@ -10,8 +10,8 @@ const (
boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1"
boundMovieBytes = int64(22846532)
boundDllSHA256 = "CF2A0C4626DF2A5D056DC87CFB3D6AA07BC7A4EF7888B5B3B80A006BAD2204AF"
boundDllBytes = int64(1229824)
boundDllSHA256 = "F6DDA53482B6B8F6A3FD155FF0A15EB590C5AFF9572140004242793DC957561C"
boundDllBytes = int64(1230336)
)
@@ -63,6 +63,16 @@ type currentDungeonCompanionRun struct {
Enabled bool
BalanceEnabled bool
RandomEquipmentEnabled bool
TeamMode bool
TeamLeaderID uint16
TeamSize int
Online bool
OnlineAt time.Time
OfflineAt time.Time
OfflineReason string
LogoutSent bool
NativePartyID uint16
NativePartySent bool
Ready bool
Reason string
ReasonMessage string
@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"os"
"sort"
"strconv"
@@ -369,6 +370,22 @@ func mustProjectionCharacterID(value string) uint64 {
return parsed
}
// buildCurrentDungeonCompanionOfflineBody is the exact present=0 projection
// body. The client's decoder requires all identity/count fields to be zero,
// the body length to be exactly the 72-byte header, and a non-zero revision.
// One body clears the whole squad, so the caller sends it once per run.
func buildCurrentDungeonCompanionOfflineBody(revision uint64) []byte {
if revision == 0 {
revision = 1
}
body := make([]byte, currentDungeonCompanionProjectionHeaderSize)
binary.LittleEndian.PutUint32(body[0:4], currentDungeonCompanionProjectionMagic)
body[4] = currentDungeonCompanionProjectionVersion
body[5] = 0
binary.LittleEndian.PutUint64(body[8:16], revision)
return body
}
func (s *Service) currentDungeonCompanionProjectionSourceID(
ctx context.Context,
session *gameSession,
@@ -531,6 +548,22 @@ func (s *Service) sendCurrentDungeonCompanionProjection(
"private_opcode", currentDungeonCompanionProjectionOpcode,
"body_len", len(body),
"template_mutation", false)
return s.sendGameUpperRawClass(
session, currentDungeonCompanionProjectionOpcode, body, 0)
if err := s.sendGameUpperRawClass(
session, currentDungeonCompanionProjectionOpcode, body, 0); err != nil {
return err
}
// The dungeon is now loaded on the client (projection is sent after op29 and
// player placement), so a notice here is visible instead of being swallowed
// by the loading screen.
if repos.Settings != nil {
if snapshot, found := loadCompanionPanelSnapshot(ctx, repos,
strconv.FormatUint(uint64(sourceID), 10)); found {
_ = s.sendCurrentClientNotice(session,
fmt.Sprintf("支援兵面板 HP=%d MP=%d 力量=%d 物攻=%d 物防=%d",
snapshot.HPMax, snapshot.MPMax, snapshot.Strength,
snapshot.PhysicalAttack, snapshot.PhysicalDefense),
currentClientNoticeDefaultColor, "companion-panel-summary")
}
}
return nil
}
@@ -0,0 +1,544 @@
package dnfbridge
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/dungeoncmd"
)
// Headless volunteer sessions.
//
// These are real game sessions from the server's point of view: they are in
// Service.gameSessions, have a character generation, a native party peer
// endpoint, and can be driven through the ordinary party/dungeon owners. Their
// transport is a sink: outbound packets are accepted and discarded because no
// client is attached. The owner's real client receives the normal party
// prelude (op11/op9/op153 + per-member mode0/mode1) and therefore sees real
// party actors, not the APC projection.
//
// This path is gated and OFF by default while the AIC action driver is not yet
// attached.
const (
currentCompanionHeadlessEnv = "DNF_COMPANION_HEADLESS"
currentCompanionHeadlessFlagFile = "companion_headless.enabled"
currentCompanionHeadlessPortBase = 45000
)
func currentCompanionHeadlessEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentCompanionHeadlessEnv))) {
case "0", "off", "false", "no":
return false
}
if _, err := os.Stat(currentCompanionHeadlessFlagFile); err == nil {
return true
}
return false
}
func headlessDebug(message string) {
file, err := os.OpenFile("companion_headless_debug.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return
}
defer file.Close()
_, _ = file.WriteString(time.Now().Format("15:04:05.000") + " " + message + "\n")
}
type companionHeadlessConn struct {
mu sync.Mutex
closed bool
port int
}
func (c *companionHeadlessConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c *companionHeadlessConn) Write(data []byte) (int, error) {
return len(data), nil
}
func (c *companionHeadlessConn) Close() error {
c.mu.Lock()
c.closed = true
c.mu.Unlock()
return nil
}
func (c *companionHeadlessConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: c.port}
}
func (c *companionHeadlessConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: c.port}
}
func (c *companionHeadlessConn) SetDeadline(time.Time) error { return nil }
func (c *companionHeadlessConn) SetReadDeadline(time.Time) error { return nil }
func (c *companionHeadlessConn) SetWriteDeadline(time.Time) error { return nil }
func (s *Service) ensureHeadlessCompanionSessions(
ctx context.Context,
ownerSession *gameSession,
sources []string,
accountID string,
) []uint16 {
if s == nil || ownerSession == nil || len(sources) == 0 {
return nil
}
headlessDebug(fmt.Sprintf("ensure_sessions owner=%d sources=%v", ownerSession.selectedCharacterID, sources))
created := make([]uint16, 0, len(sources))
for _, raw := range sources {
id, ok := currentDungeonCompanionProjectionSourceIDValue(raw)
if !ok || id == ownerSession.selectedCharacterID {
continue
}
if real, online := s.onlineGameSession(id); online && real != nil {
if !real.companionHeadless {
continue
}
created = append(created, id)
continue
}
session := &gameSession{
conn: &companionHeadlessConn{port: currentCompanionHeadlessPortBase + int(id)},
connID: "companion-headless-" + strconv.FormatUint(uint64(id), 10),
accountID: accountID,
selectedCharacterID: id,
characterGeneration: s.nextCompanionGeneration(),
channel: ownerSession.channel,
companionHeadless: true,
companionVirtual: false,
companionOwnerID: ownerSession.selectedCharacterID,
}
session.partyPeerMu.Lock()
session.partyPeer = currentPartyPeerEndpointRegistration{
InnerIPv4: [4]byte{127, 0, 0, 1},
OuterIPv4: [4]byte{127, 0, 0, 1},
Port: uint16(currentCompanionHeadlessPortBase + int(id)),
MTU: currentPartyPeerDefaultPathMTU,
}
session.partyRoutingKey = s.nextPartyRoutingKey.Add(1)
session.partyPathMTU = currentPartyPeerDefaultPathMTU
session.partyPeerMu.Unlock()
session.townMu.Lock()
session.townPositionSnapshot = ownerSession.townPositionSnapshot
session.townMu.Unlock()
s.mu.Lock()
if s.gameSessions == nil {
s.gameSessions = make(map[uint16]*gameSession)
}
s.gameSessions[id] = session
s.mu.Unlock()
go s.runHeadlessCompanionAIC(session)
created = append(created, id)
}
if len(created) != 0 {
headlessDebug(fmt.Sprintf("sessions_created ids=%v", created))
s.logGameEvent(ownerSession, "game-companion-headless-sessions-online",
"owner_character_id", ownerSession.selectedCharacterID,
"character_ids", uint16ListLog(created))
}
return created
}
// dropHeadlessCompanionSessionsForOwner removes the previous login's headless
// sessions when the feature is switched off. Without this, a relog after
// deleting the flag would leave the synthetic party members in the manager and
// the client would stay at "connecting".
func (s *Service) dropHeadlessCompanionSessionsForOwner(ownerID uint16) {
if s == nil || ownerID == 0 {
return
}
manager := s.runtimePartyManagerForService()
removed := make([]uint16, 0, 4)
type removedMember struct {
id uint16
generation uint64
}
members := make([]removedMember, 0, 4)
s.mu.Lock()
for id, session := range s.gameSessions {
if session == nil || !session.companionHeadless || session.companionOwnerID != ownerID {
continue
}
delete(s.gameSessions, id)
removed = append(removed, id)
members = append(members, removedMember{id: id, generation: session.characterGeneration})
}
s.mu.Unlock()
if manager != nil {
for _, member := range members {
manager.Leave(member.id, member.generation)
}
}
if len(removed) != 0 {
s.logPacketEvent("game-companion-headless-sessions-dropped",
"owner_character_id", ownerID,
"character_ids", uint16ListLog(removed))
}
}
// CompanionForceEnter drives the ordinary op16 dungeon entry for an online
// character without requiring the client to send op16. It exists for the
// headless-party PoC: the client's party window can be stuck on its P2P gate
// even though the server party is complete, which blocks the only packet that
// would start the entry.
func (s *Service) CompanionForceEnter(characterID uint16, dungeonID uint32) error {
if s == nil || characterID == 0 || dungeonID == 0 {
return fmt.Errorf("companion force enter needs character and dungeon")
}
session, online := s.onlineGameSession(characterID)
if !online || session == nil {
return fmt.Errorf("character %d is not online", characterID)
}
body := make([]byte, dungeoncmd.SelectDungeonRequestSize)
binary.LittleEndian.PutUint32(body[0:4], dungeonID)
body[4] = 0 // difficulty
binary.LittleEndian.PutUint16(body[5:7], 0)
body[7] = 0 // selection mode
body[8] = 0 // runtime state
binary.LittleEndian.PutUint16(body[9:11], 0)
binary.LittleEndian.PutUint32(body[11:15], 0)
body[15] = 0 // the current 21-byte writer rejects a non-zero value here
binary.LittleEndian.PutUint32(body[16:20],
uint32(currentSceneActorObjectKey(characterID)))
body[20] = 0
session.companionForceEnter = true
defer func() { session.companionForceEnter = false }()
s.ensureForceSelectorOrigin(session)
headlessDebug(fmt.Sprintf("force_enter character=%d dungeon=%d", characterID, dungeonID))
return s.handleDungeonSelectUpper(session, body)
}
func (s *Service) ensureForceSelectorOrigin(session *gameSession) bool {
if s == nil || session == nil {
return false
}
session.townMu.Lock()
if session.townSelectorOriginBound {
session.townMu.Unlock()
return true
}
origin := session.townPositionSnapshot
session.townMu.Unlock()
// The live town snapshot carries the current area (for example 38/2), while
// the dungeon return origin is validated against the durable character
// location (38/1). Use the durable ids and only borrow x/y from the live
// position.
if repos, ok := s.repositoryGroup(); ok && repos.Character != nil {
if record, found, err := repos.Character.Load(context.Background(),
strconv.FormatUint(uint64(session.selectedCharacterID), 10)); err == nil && found {
townID := byte(numericCharacterStatValue(record, "town_id"))
areaID := byte(numericCharacterStatValue(record, "area_id"))
if townID == 0 {
townID = origin.TownID
}
if areaID == 0 {
areaID = origin.AreaID
}
origin.TownID = townID
origin.AreaID = areaID
if !origin.PositionValid {
origin.PositionX = uint16(numericCharacterStatValue(record, "pos_x"))
origin.PositionY = uint16(numericCharacterStatValue(record, "pos_y"))
origin.MovementCode = byte(numericCharacterStatValue(record, "direction"))
origin.PositionValid = true
}
}
}
if !origin.PositionValid {
headlessDebug(fmt.Sprintf("force_origin_unavailable char=%d", session.selectedCharacterID))
return false
}
origin.CharacterID = session.selectedCharacterID
session.townMu.Lock()
session.townSelectorOriginSnapshot = origin
session.townSelectorOriginBound = true
session.townMu.Unlock()
headlessDebug(fmt.Sprintf("force_origin_bound char=%d town=%d area=%d x=%d y=%d",
session.selectedCharacterID, origin.TownID, origin.AreaID, origin.PositionX, origin.PositionY))
return true
}
func (s *Service) bindHeadlessSelectorOrigin(leader, follower *gameSession) bool {
if s == nil || leader == nil || follower == nil {
return false
}
leader.townMu.Lock()
origin := leader.townSelectorOriginSnapshot
bound := leader.townSelectorOriginBound
leader.townMu.Unlock()
if !bound || !origin.PositionValid {
headlessDebug(fmt.Sprintf("selector_origin_missing char=%d bound=%v valid=%v",
follower.selectedCharacterID, bound, origin.PositionValid))
return false
}
origin.CharacterID = follower.selectedCharacterID
follower.townMu.Lock()
follower.townSelectorOriginSnapshot = origin
follower.townSelectorOriginBound = true
follower.townMu.Unlock()
headlessDebug(fmt.Sprintf("selector_origin_bound char=%d town=%d area=%d x=%d y=%d",
follower.selectedCharacterID, origin.TownID, origin.AreaID, origin.PositionX, origin.PositionY))
return true
}
func (s *Service) ensureHeadlessPartyActorProjections(ctx context.Context, leader *gameSession, runtime *runtimeDungeonState) {
if s == nil || runtime == nil || runtime.Companion == nil {
return
}
for _, entry := range runtime.Companion.Sources {
if !entry.Ready || entry.SourceCharacterID == 0 {
continue
}
identity, online := s.onlineGameSessionCharacterSnapshot(entry.SourceCharacterID)
if !online || identity.session == nil || !identity.session.companionHeadless {
continue
}
if _, found := currentPartyActorProjectionSnapshot(
identity.session, entry.SourceCharacterID, identity.generation); found {
continue
}
state := entry.ProgressionSnapshot
if state.Source.CharacterID == "" {
headlessDebug(fmt.Sprintf("actor_projection_missing_state char=%d", entry.SourceCharacterID))
continue
}
name := strings.TrimSpace(state.Source.Name)
if name == "" {
name = entry.SourceName
}
ownerChannel := currentPartySceneOwnerContext(identity.session)
mode0, err := s.buildCurrentSceneObjectListBodyForSessionInContextStrict(
ctx, identity.session, entry.SourceCharacterID, name, state.Source, true, ownerChannel)
if err != nil || len(mode0) == 0 {
headlessDebug(fmt.Sprintf("actor_projection_mode0_failed char=%d err=%v", entry.SourceCharacterID, err))
continue
}
reader := csharpLegacyUserInfoReader{
ctx: ctx,
characterID: state.Source.CharacterID,
service: s,
session: identity.session,
suppressActorSkillResources: true,
equipmentSnapshot: &state.Equipment,
}
mode1 := reader.buildCurrentUserInfoMode1WithEquipmentForOwner(
state.Source, true, entry.SourceCharacterID, true, 0,
currentSceneObjectRoute, ownerChannel)
if len(mode1) == 0 {
headlessDebug(fmt.Sprintf("actor_projection_mode1_failed char=%d", entry.SourceCharacterID))
continue
}
if err := captureCurrentPartyActorProjection(
identity.session, entry.SourceCharacterID, mode0, mode1); err != nil {
headlessDebug(fmt.Sprintf("actor_projection_capture_failed char=%d err=%v", entry.SourceCharacterID, err))
continue
}
headlessDebug(fmt.Sprintf("actor_projection_captured char=%d owner=%d mode0=%d mode1=%d",
entry.SourceCharacterID, ownerChannel, len(mode0), len(mode1)))
}
}
func (s *Service) publishHeadlessActorToLeader(headless *gameSession, runtime *runtimeDungeonState) bool {
if s == nil || headless == nil || runtime == nil || runtime.partyDungeonOwner == nil {
return false
}
leader, online := s.onlineGameSession(runtime.partyDungeonOwner.leaderID)
if !online || leader == nil {
return false
}
projection, found := currentPartyActorProjectionSnapshot(
headless, headless.selectedCharacterID, headless.characterGeneration)
if !found {
return false
}
for _, body := range [][]byte{projection.mode0, projection.mode1} {
if len(body) == 0 {
continue
}
if err := s.sendGameUpperRawClass(
leader, uint16(dnfenum.CmdPacketSetUDPIPPort), body, 0); err != nil {
return false
}
}
leader.townMu.Lock()
position := leader.townPositionSnapshot
leader.townMu.Unlock()
x := int(position.PositionX) + int(headless.selectedCharacterID%3)*40
y := int(position.PositionY)
area := buildCurrentTownUserAreaNotificationBody(
currentSceneActorObjectKey(headless.selectedCharacterID),
position.TownID, 0xff, uint16(x), uint16(y), position.MovementCode,
currentActorDefaultUserStateBits)
if err := s.sendGameUpperRawClass(leader, currentTownUserAreaNotificationMsgID, area, 0); err != nil {
return false
}
move := buildCurrentTownUserPositionNotificationBody(
currentSceneActorObjectKey(headless.selectedCharacterID),
uint16(x), uint16(y), position.MovementCode, 100)
if err := s.sendGameUpperRawClass(leader, currentTownUserPositionNotificationMsgID, move, 0); err != nil {
return false
}
headlessDebug(fmt.Sprintf("headless_actor_projected char=%d leader=%d mode0=%d mode1=%d x=%d y=%d",
headless.selectedCharacterID, leader.selectedCharacterID, len(projection.mode0), len(projection.mode1), x, y))
return true
}
func (s *Service) publishHeadlessActorsBeforeStart(leader *gameSession, runtime *runtimeDungeonState) {
if s == nil || leader == nil || runtime == nil || runtime.partyDungeonOwner == nil {
return
}
for _, member := range runtime.partyDungeonOwner.members {
identity, online := s.onlineGameSessionCharacterSnapshot(member.userID)
if !online || identity.session == nil || !identity.session.companionHeadless ||
identity.session.headlessActorPublished {
continue
}
if s.publishHeadlessActorToLeader(identity.session, runtime) {
identity.session.headlessActorPublished = true
}
}
}
func (s *Service) ensureHeadlessCompanionParty(ownerSession *gameSession, ids []uint16) {
if s == nil || ownerSession == nil || len(ids) == 0 {
return
}
manager := s.runtimePartyManagerForService()
if manager == nil {
return
}
ownerIdentity, ok := s.boundGameSessionCharacterSnapshot(ownerSession)
if !ok {
headlessDebug("party owner_identity_missing")
return
}
ownerMember, ok := s.runtimePartyMemberForIdentity(ownerIdentity)
if !ok {
headlessDebug(fmt.Sprintf("party owner_member_invalid owner=%d gen=%d", ownerIdentity.character, ownerIdentity.generation))
return
}
snapshot, found := manager.SnapshotByUser(ownerIdentity.character, ownerIdentity.generation)
headlessDebug(fmt.Sprintf("party snapshot_before found=%v id=%d members=%d", found, snapshot.ID, len(snapshot.Members)))
if !found {
created := manager.EnsureLeader(ownerMember, runtimePartyStateSnapshot(ownerSession))
if !created.OK {
headlessDebug("party ensure_leader_failed reason=" + created.Reason)
s.logGameEvent(ownerSession, "game-companion-headless-party-failed",
"stage", "leader", "reason", created.Reason)
return
}
snapshot = created.Party
} else if len(snapshot.Members) > 1 {
// A real party already owns this roster; never mix headless volunteers in.
headlessDebug(fmt.Sprintf("party real_party_present members=%d", len(snapshot.Members)))
return
}
for _, id := range ids {
identity, online := s.onlineGameSessionCharacterSnapshot(id)
if !online || identity.session == nil || !identity.session.companionHeadless {
headlessDebug(fmt.Sprintf("party join_skip id=%d online=%v", id, online))
continue
}
member, memberOK := s.runtimePartyMemberForIdentity(identity)
if !memberOK {
headlessDebug(fmt.Sprintf("party join_member_invalid id=%d", id))
continue
}
joined := manager.Join(snapshot.ID, member)
if !joined.OK {
headlessDebug(fmt.Sprintf("party join_failed id=%d reason=%s", id, joined.Reason))
s.logGameEvent(ownerSession, "game-companion-headless-party-failed",
"stage", "join", "member_id", id, "reason", joined.Reason)
continue
}
snapshot = joined.Party
headlessDebug(fmt.Sprintf("party joined id=%d members=%d", id, len(snapshot.Members)))
}
if len(snapshot.Members) <= 1 {
headlessDebug("party no_members_after_join")
return
}
state := s.publishRuntimePartySnapshot(snapshot)
headlessDebug(fmt.Sprintf("party published party_id=%d members=%d", state.PartyID, len(snapshot.Members)))
if state.PartyID <= 0 {
s.logGameEvent(ownerSession, "game-companion-headless-party-failed",
"stage", "publish")
return
}
s.logGameEvent(ownerSession, "game-companion-headless-party-online",
"owner_character_id", ownerSession.selectedCharacterID,
"party_id", state.PartyID,
"member_count", len(snapshot.Members))
}
// runHeadlessCompanionAIC is the server-side action driver for one headless
// volunteer. The first milestone commits real monster deaths as that character:
// the existing op39 owner validates the room actor, applies reward/clear state,
// and broadcasts the death to every real party member, so the player sees the
// volunteer actually clearing the room. Movement/skill choreography is a later
// layer on top of the same driver.
func (s *Service) runHeadlessCompanionAIC(session *gameSession) {
if s == nil || session == nil {
return
}
ticker := time.NewTicker(1500 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
if session.selectedCharacterID == 0 {
return
}
session.dungeon.mu.Lock()
runtime := session.dungeon.runtime
if runtime == nil || runtime.Room == nil || runtime.partyDungeonOwner == nil {
session.dungeon.mu.Unlock()
continue
}
snapshot := runtime.Room.Snapshot()
session.dungeon.mu.Unlock()
// A real follower sends op37 FINISH_LOADING after every room load; the
// party owner's loading gate waits for every member before it releases
// the leader. The headless session has no client, so issue the same
// request itself. The handler is idempotent per loading cycle.
if !session.headlessActorPublished {
if s.publishHeadlessActorToLeader(session, runtime) {
session.headlessActorPublished = true
}
}
_ = s.sendFinishLoadingStatus(session, make([]byte, currentFinishLoadingLegacyRequestBodySize))
var target uint32
for _, monster := range snapshot.Monsters {
if monster.State == runtimeDungeonMonsterAnnounced {
target = monster.ObjectKey
break
}
}
if target == 0 {
continue
}
body := make([]byte, dungeoncmd.DieMonsterRequestSize)
binary.LittleEndian.PutUint32(body[0:4], target)
binary.LittleEndian.PutUint16(body[4:6],
currentSceneActorObjectKey(session.selectedCharacterID))
if err := s.handleDungeonMonsterDeath(session, body); err != nil {
s.logGameEvent(session, "game-companion-headless-ai-death-failed",
"character_id", session.selectedCharacterID,
"object_key", target,
"error", err)
continue
}
s.logGameEvent(session, "game-companion-headless-ai-action",
"character_id", session.selectedCharacterID,
"action", "kill_monster",
"object_key", target,
"room", snapshot.Coordinate.String(),
"map_id", snapshot.MapID)
}
}
@@ -0,0 +1,430 @@
package dnfbridge
import (
"context"
"os"
"strconv"
"strings"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/party"
dnffriend "longheng.io/server/internal/modules/dnf/unitedfriend"
)
// Volunteers are persistent social characters: the native friend list and the
// native party frame both resolve them through a synthetic presence session.
// The session has no socket; it exists only while the server process is up so
// the operator's friend window keeps seeing the volunteers as online, including
// after a dungeon ends.
const (
currentCompanionNativePartyEnv = "DNF_COMPANION_NATIVE_PARTY"
// Presence of this file disables the native op9/op153 frame without a
// restart. The friend presence stays online; only the party window is
// suppressed, which is the fast rollback if a client build rejects the
// synthetic roster.
currentCompanionNativePartyDisableFile = "companion_native_party.disabled"
currentCompanionVirtualPrefix = "companion-presence-"
)
func currentCompanionNativePartyEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentCompanionNativePartyEnv))) {
case "0", "off", "false", "no":
return false
}
if _, err := os.Stat(currentCompanionNativePartyDisableFile); err == nil {
return false
}
return true
}
func (s *Service) companionPresenceSession(characterID uint16) (*gameSession, bool) {
if s == nil || characterID == 0 {
return nil, false
}
// A real client always wins over a synthetic presence row.
if real, online := s.onlineGameSession(characterID); online && real != nil {
return real, false
}
s.companionPresenceMu.Lock()
defer s.companionPresenceMu.Unlock()
session := s.companionSessions[characterID]
return session, session != nil
}
// companionPresenceFriendChannel returns the friend-row channel for a synthetic
// volunteer. It copies the field under companionPresenceMu because ensure may
// update the channel when the owner moves.
func (s *Service) companionPresenceFriendChannel(characterID uint16) (uint16, bool) {
if s == nil || characterID == 0 {
return 0, false
}
if real, online := s.onlineGameSession(characterID); online && real != nil {
return 0, false
}
s.companionPresenceMu.Lock()
defer s.companionPresenceMu.Unlock()
session := s.companionSessions[characterID]
if session == nil {
return 0, false
}
return uint16(session.channel.ID), true
}
func (s *Service) companionSessionSnapshot(characterID uint16) (boundGameSessionCharacter, bool) {
session, ok := s.companionPresenceSession(characterID)
if !ok || session == nil || session.companionVirtual == false {
return boundGameSessionCharacter{}, false
}
return boundGameSessionCharacter{
session: session,
character: characterID,
generation: session.characterGeneration,
virtual: true,
}, true
}
func (s *Service) companionSessionCurrent(identity boundGameSessionCharacter) bool {
if s == nil || identity.session == nil || identity.character == 0 {
return false
}
s.companionPresenceMu.Lock()
defer s.companionPresenceMu.Unlock()
return s.companionSessions[identity.character] == identity.session &&
identity.session.selectedCharacterID == identity.character &&
identity.session.characterGeneration == identity.generation
}
func (s *Service) nextCompanionGeneration() uint64 {
s.mu.Lock()
defer s.mu.Unlock()
s.nextGameSessionGeneration++
if s.nextGameSessionGeneration == 0 {
s.nextGameSessionGeneration++
}
return s.nextGameSessionGeneration
}
// ensureCompanionPresenceForSession makes the currently configured volunteers
// visible in the native friend window for this owner. It is called at the login
// scene tail (before op273) and again at dungeon entry.
func (s *Service) ensureCompanionPresenceForSession(ctx context.Context, session *gameSession) {
if s == nil || session == nil || session.selectedCharacterID == 0 ||
!currentDungeonCompanionTeamModeEnabled() {
return
}
if ctx == nil {
ctx = context.Background()
}
repos, ok := s.repositoryGroup()
if !ok || repos.Character == nil || repos.Settings == nil {
return
}
ownerID := session.selectedCharacterID
ownerKey := strconv.FormatUint(uint64(ownerID), 10)
cfg, _, _, err := s.loadCurrentDungeonCompanionConfig(
ctx, ownerKey, strings.TrimSpace(s.accountIDForSession(session)), repos)
if err != nil {
s.logGameEvent(session, "game-companion-presence-config-failed",
"owner_character_id", ownerID, "error", err)
return
}
if !cfg.Enabled {
s.dropCompanionPresenceForOwner(ownerID)
return
}
sources := cfg.Sources()
accountID := strings.TrimSpace(s.accountIDForSession(session))
if accountID == "" {
owner, found, loadErr := repos.Character.Load(ctx, ownerKey)
if loadErr != nil || !found {
return
}
accountID = strings.TrimSpace(owner.AccountID)
}
matched, _, applied := s.autoMatchCurrentDungeonCompanionSources(
ctx, repos, accountID, ownerID, sources)
if applied {
sources = matched
}
if len(sources) == 0 {
return
}
ownerName := ""
if _, name, _, found := s.selectedCharacterForEnter(ctx, session); found {
ownerName = name
}
s.ensureCompanionFriendRelations(ctx, session, ownerName, sources)
if currentCompanionHeadlessEnabled() {
ids := s.ensureHeadlessCompanionSessions(ctx, session, sources, accountID)
s.ensureHeadlessCompanionParty(session, ids)
} else {
s.dropHeadlessCompanionSessionsForOwner(ownerID)
s.ensureCompanionVirtualSessions(ctx, session, sources, accountID)
}
s.logGameEvent(session, "game-companion-presence-online",
"owner_character_id", ownerID,
"source_character_ids", strings.Join(sources, ","),
"count", len(sources))
}
func (s *Service) ensureCompanionVirtualSessions(
ctx context.Context,
ownerSession *gameSession,
sources []string,
accountID string,
) {
if s == nil || ownerSession == nil || len(sources) == 0 {
return
}
repos, ok := s.repositoryGroup()
if !ok || repos.Character == nil {
return
}
for _, raw := range sources {
id, ok := currentDungeonCompanionProjectionSourceIDValue(raw)
if !ok || id == ownerSession.selectedCharacterID {
continue
}
if _, realOnline := s.onlineGameSession(id); realOnline {
continue
}
character, found, err := repos.Character.Load(ctx, strconv.FormatUint(uint64(id), 10))
if err != nil || !found || character.Stats["delete_flag"] != 0 ||
strings.TrimSpace(character.Name) == "" {
continue
}
// Allocate the generation before taking companionPresenceMu: the
// generation owner lives under Service.mu, and bindGameSessionCharacter
// holds Service.mu while dropping a virtual session.
generation := s.nextCompanionGeneration()
s.companionPresenceMu.Lock()
if s.companionSessions == nil {
s.companionSessions = make(map[uint16]*gameSession)
}
session := s.companionSessions[id]
if session == nil {
session = &gameSession{
connID: currentCompanionVirtualPrefix + strconv.FormatUint(uint64(id), 10),
accountID: accountID,
selectedCharacterID: id,
characterGeneration: generation,
channel: ownerSession.channel,
companionVirtual: true,
companionOwnerID: ownerSession.selectedCharacterID,
}
s.companionSessions[id] = session
} else {
session.channel = ownerSession.channel
session.companionOwnerID = ownerSession.selectedCharacterID
session.accountID = accountID
}
s.companionPresenceMu.Unlock()
}
}
func (s *Service) ensureCompanionFriendRelations(
ctx context.Context,
session *gameSession,
ownerName string,
sources []string,
) {
if s == nil || session == nil || strings.TrimSpace(ownerName) == "" {
return
}
repos, ok := s.repositoryGroup()
if !ok || repos.UnitedFriend == nil || repos.Character == nil {
return
}
owner, err := dnffriend.NewOwner(repos, nil)
if err != nil {
return
}
ownerID := session.selectedCharacterID
s.friendMu.Lock()
defer s.friendMu.Unlock()
for _, raw := range sources {
id, ok := currentDungeonCompanionProjectionSourceIDValue(raw)
if !ok || id == ownerID {
continue
}
character, found, loadErr := repos.Character.Load(ctx, strconv.FormatUint(uint64(id), 10))
if loadErr != nil || !found || strings.TrimSpace(character.Name) == "" {
continue
}
if _, addErr := owner.Add(ctx, ownerName, ownerID, character.Name, id); addErr != nil {
if addErr != dnffriend.ErrFull {
s.logGameEvent(session, "game-companion-friend-add-failed",
"owner_character_id", ownerID,
"source_character_id", id,
"error", addErr)
}
continue
}
s.logGameEvent(session, "game-companion-friend-ensured",
"owner_character_id", ownerID,
"source_character_id", id,
"name", character.Name)
}
}
func (s *Service) dropCompanionPresenceCharacter(characterID uint16) {
if s == nil || characterID == 0 {
return
}
s.companionPresenceMu.Lock()
defer s.companionPresenceMu.Unlock()
delete(s.companionSessions, characterID)
}
func (s *Service) dropCompanionPresenceForOwner(ownerID uint16) {
if s == nil || ownerID == 0 {
return
}
s.companionPresenceMu.Lock()
defer s.companionPresenceMu.Unlock()
for id, session := range s.companionSessions {
if session != nil && session.companionOwnerID == ownerID {
delete(s.companionSessions, id)
}
}
}
func companionPartyID(ownerID uint16) uint16 {
id := uint16(0xE000) | (ownerID & 0x0FFF)
if id == 0 || id == 0xFFFF {
id = 0xE001
}
return id
}
func (s *Service) buildCompanionPartyState(session *gameSession, run *currentDungeonCompanionRun) alignedcmd.PartyState {
if s == nil || session == nil || run == nil {
return alignedcmd.PartyState{}
}
state := alignedcmd.PartyState{
PartyID: int(companionPartyID(session.selectedCharacterID)),
IsLeader: true,
UserID: session.selectedCharacterID,
UserState: 100,
MaxMembers: 4,
}
state.Members = append(state.Members, alignedcmd.PartyMemberState{
UserID: session.selectedCharacterID, Slot: 0, UserState: 100, HPPercent: 100, MPPercent: 100,
})
slot := byte(1)
for _, entry := range run.Sources {
if !entry.Ready || entry.SourceCharacterID == 0 {
continue
}
if slot >= state.MaxMembers {
break
}
state.Members = append(state.Members, alignedcmd.PartyMemberState{
UserID: entry.SourceCharacterID, Slot: slot, UserState: 100, HPPercent: 100, MPPercent: 100,
})
slot++
}
return state
}
// publishCurrentDungeonCompanionNativeParty builds the native op9 party frame
// (kind-0 complete roster) followed by op153 HP/MP rows. It intentionally does
// not send op11: the volunteers have no P2P socket, and a made-up endpoint
// would leave the client stuck on "connecting". The scene actors themselves
// still come from the support-soldier projection.
func (s *Service) publishCurrentDungeonCompanionNativeParty(session *gameSession, runtime *runtimeDungeonState) {
if s == nil || session == nil || runtime == nil || runtime.Companion == nil ||
currentCompanionHeadlessEnabled() ||
!currentCompanionNativePartyEnabled() {
return
}
run := runtime.Companion
if !run.Ready || len(run.Sources) == 0 {
return
}
currentParty := runtimePartyStateSnapshot(session)
if currentParty.PartyID > 0 && len(runtimePartyMembers(currentParty)) > 1 {
s.logGameEvent(session, "game-companion-native-party-skipped",
"owner_character_id", session.selectedCharacterID,
"reason", "owner_already_in_native_party")
return
}
if runtime.partyDungeonOwner != nil || isCurrentImplementedRaidChannel(session) {
s.logGameEvent(session, "game-companion-native-party-skipped",
"owner_character_id", session.selectedCharacterID,
"reason", "party_or_raid_owner_has_priority")
return
}
sources := make([]string, 0, len(run.Sources))
for _, entry := range run.Sources {
if entry.Ready && entry.SourceCharacterID != 0 {
sources = append(sources, strconv.FormatUint(uint64(entry.SourceCharacterID), 10))
}
}
s.ensureCompanionVirtualSessions(
context.Background(), session, sources, strings.TrimSpace(s.accountIDForSession(session)))
state := s.buildCompanionPartyState(session, run)
if len(state.Members) <= 1 {
return
}
sent, err := s.sendCurrentPartyActorFrameProjection(
session, session, state, "companion_native_party")
if err != nil {
s.logGameEvent(session, "game-companion-native-party-failed",
"owner_character_id", session.selectedCharacterID,
"party_id", state.PartyID,
"error", err)
return
}
if !sent {
s.logGameEvent(session, "game-companion-native-party-deferred",
"owner_character_id", session.selectedCharacterID,
"party_id", state.PartyID,
"reason", "op9_projection_not_delivered")
return
}
if err := s.sendRuntimePartyRealtimeInfoLocal(session, state); err != nil {
s.logGameEvent(session, "game-companion-native-party-rows-failed",
"owner_character_id", session.selectedCharacterID,
"party_id", state.PartyID,
"error", err)
return
}
run.NativePartyID = uint16(state.PartyID)
run.NativePartySent = true
s.logGameEvent(session, "game-companion-native-party-sent",
"owner_character_id", session.selectedCharacterID,
"party_id", state.PartyID,
"member_count", len(state.Members),
"source_character_ids", uint16ListLog(currentDungeonCompanionTeamSourceIDs(run)))
}
// clearCurrentDungeonCompanionNativeParty empties only the party window; the
// virtual sessions and friend rows stay online, which is the requested
// post-dungeon state.
func (s *Service) clearCurrentDungeonCompanionNativeParty(session *gameSession, run *currentDungeonCompanionRun) {
if s == nil || session == nil || run == nil || !run.NativePartySent || run.NativePartyID == 0 {
return
}
body := buildCurrentSceneOp9ActorRemovalBodyInContext(
run.NativePartyID, currentPartySceneOwnerContext(session))
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketRecoverStamina), body, 0); err != nil {
s.logGameEvent(session, "game-companion-native-party-clear-failed",
"owner_character_id", session.selectedCharacterID,
"party_id", run.NativePartyID,
"error", err)
}
if err := s.sendGameUpperRawClassCodec(
session, 0x0099, party.BuildEmptyRealtimeInfo(), 0, true); err != nil {
s.logGameEvent(session, "game-companion-native-party-clear-rows-failed",
"owner_character_id", session.selectedCharacterID,
"party_id", run.NativePartyID,
"error", err)
}
run.NativePartySent = false
s.logGameEvent(session, "game-companion-native-party-cleared",
"owner_character_id", session.selectedCharacterID,
"party_id", run.NativePartyID,
"friends_online", true)
}
@@ -0,0 +1,129 @@
package dnfbridge
import (
"bytes"
"encoding/binary"
"net"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/channelcatalog"
)
type companionPartyTestConn struct {
net.Conn
write bytes.Buffer
}
func (c *companionPartyTestConn) Write(data []byte) (int, error) { return c.write.Write(data) }
func (c *companionPartyTestConn) SetWriteDeadline(time.Time) error {
return nil
}
func TestCompanionPresenceFriendRowRendersOnline(t *testing.T) {
service := &Service{}
virtual := &gameSession{
selectedCharacterID: 6,
characterGeneration: 7,
channel: channelcatalog.Channel{ID: 11},
companionVirtual: true,
companionOwnerID: 2,
}
service.companionSessions = map[uint16]*gameSession{6: virtual}
presence := service.currentUnitedFriendPresence(6)
if presence.Busy {
t.Fatal("volunteer friend row must not render busy/登录中")
}
if presence.ChannelID != 11 {
t.Fatalf("friend row channel=%d want 11", presence.ChannelID)
}
}
func TestBuildCompanionPartyStateUsesNativeFourSlotRoster(t *testing.T) {
service := &Service{}
owner := &gameSession{selectedCharacterID: 2}
run := &currentDungeonCompanionRun{
Ready: true,
Sources: []currentDungeonCompanionSourceRun{
{Ready: true, SourceCharacterID: 6},
{Ready: false, SourceCharacterID: 7},
{Ready: true, SourceCharacterID: 3},
},
}
state := service.buildCompanionPartyState(owner, run)
if state.PartyID <= 0 || state.PartyID >= 0xFFFF {
t.Fatalf("party id=%d is not a printable native party", state.PartyID)
}
if state.UserID != 2 || !state.IsLeader || state.MaxMembers != 4 {
t.Fatalf("party identity=%+v", state)
}
if len(state.Members) != 3 {
t.Fatalf("members=%v want owner + 2 ready volunteers", state.Members)
}
want := []struct {
id uint16
slot byte
}{{2, 0}, {6, 1}, {3, 2}}
for index, expected := range want {
member := state.Members[index]
if member.UserID != expected.id || member.Slot != expected.slot {
t.Fatalf("member %d=%+v want id=%d slot=%d", index, member, expected.id, expected.slot)
}
if member.HPPercent != 100 || member.MPPercent != 100 {
t.Fatalf("member %d hp/mp=%d/%d want 100/100", index, member.HPPercent, member.MPPercent)
}
}
}
func TestClearCompanionNativePartySendsOp9ThenOp153(t *testing.T) {
connection := &companionPartyTestConn{}
session := &gameSession{
conn: connection,
connID: "companion-party-clear",
selectedCharacterID: 2,
characterGeneration: 3,
channel: channelcatalog.Channel{ID: 11},
}
run := &currentDungeonCompanionRun{
Ready: true,
NativePartyID: 0xE002,
NativePartySent: true,
}
service := &Service{}
service.clearCurrentDungeonCompanionNativeParty(session, run)
if run.NativePartySent {
t.Fatal("native party flag was not cleared")
}
stream := connection.write.Bytes()
headerSize := service.gameUpperHeaderSize()
msgIDs := make([]uint16, 0, 2)
for len(stream) > 0 {
if len(stream) < headerSize {
t.Fatalf("truncated packet header: %d", len(stream))
}
total := int(binary.LittleEndian.Uint32(stream[3:7]))
if total < headerSize || total > len(stream) {
t.Fatalf("packet total=%d stream=%d", total, len(stream))
}
msgIDs = append(msgIDs, binary.LittleEndian.Uint16(stream[1:3]))
stream = stream[total:]
}
if len(msgIDs) != 2 || msgIDs[0] != 9 || msgIDs[1] != 0x0099 {
t.Fatalf("clear packets=%v want [9 153]", msgIDs)
}
}
func TestCompanionSessionIdentityIsCurrentOnlyWhileRegistered(t *testing.T) {
service := &Service{}
virtual := &gameSession{selectedCharacterID: 6, characterGeneration: 9, companionVirtual: true}
service.companionSessions = map[uint16]*gameSession{6: virtual}
identity, ok := service.companionSessionSnapshot(6)
if !ok || !identity.virtual || !service.boundGameSessionCharacterCurrent(identity) {
t.Fatalf("virtual identity=%+v ok=%v current=%v", identity, ok, service.boundGameSessionCharacterCurrent(identity))
}
delete(service.companionSessions, 6)
if service.boundGameSessionCharacterCurrent(identity) {
t.Fatal("removed volunteer session must fail the liveness check")
}
}
@@ -0,0 +1,306 @@
package dnfbridge
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
dnfcharstat "longheng.io/server/internal/modules/dnf/charstat"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// Companion panel snapshot (Option B): the client captures the source
// character's final character-info panel values and stores them in a settings
// record; the support-soldier projection prefers this snapshot over the
// PVF-derived base vector so the companion can mirror the source panel.
const (
companionPanelSnapshotKeyPrefix = "character:"
companionPanelSnapshotKeySuffix = ":companion-panel-v1"
)
var errCompanionPanelCharacterID = errors.New("companion panel: character_id is required")
type companionPanelSnapshot struct {
HPMax int64
MPMax int64
Strength int64
Intelligence int64
Vitality int64
Spirit int64
PhysicalAttack int64
MagicalAttack int64
IndependentAttack int64
PhysicalDefense int64
MagicalDefense int64
FireResistance int64
WaterResistance int64
DarkResistance int64
LightResistance int64
MoveSpeed int64
AttackSpeed int64
CastSpeed int64
HitRecovery int64
JumpPower int64
Weight int64
InventoryLimit int64
HPRegenSpeed int64
MPRegenSpeed int64
SourceClient string
// The following values are shown on the character-info panel but are not
// part of the 92-byte companion mode1 blob; they are stored so the GM panel
// can mirror the full panel and future projections can consume them.
// Percentages are stored scaled by 10 (70.0% -> 700); the three speed fields
// above use the blob's scale of 100 (60.50% -> 6050).
PhysicalCritical int64
MagicalCritical int64
MagicResistance int64
HitRate int64
DodgeRate int64
Stiffness int64
FireDamage int64
WaterDamage int64
LightDamage int64
DarkDamage int64
}
func companionPanelSnapshotScope(characterID string) string {
return companionPanelSnapshotKeyPrefix + strings.TrimSpace(characterID) +
companionPanelSnapshotKeySuffix
}
func companionPanelSnapshotInt(values map[string]string, names ...string) int64 {
for _, name := range names {
raw, ok := values[name]
if !ok {
continue
}
parsed, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err == nil {
return parsed
}
}
return 0
}
func loadCompanionPanelSnapshot(
ctx context.Context,
repos dnfrepo.Group,
characterID string,
) (companionPanelSnapshot, bool) {
if repos.Settings == nil || strings.TrimSpace(characterID) == "" {
return companionPanelSnapshot{}, false
}
record, found, err := repos.Settings.Load(
ctx, companionPanelSnapshotScope(characterID))
if err != nil || !found {
return companionPanelSnapshot{}, false
}
values := record.Values
snapshot := companionPanelSnapshot{
HPMax: companionPanelSnapshotInt(values, "hp_max"),
MPMax: companionPanelSnapshotInt(values, "mp_max"),
Strength: companionPanelSnapshotInt(values, "strength"),
Intelligence: companionPanelSnapshotInt(values, "intelligence"),
Vitality: companionPanelSnapshotInt(values, "vitality"),
Spirit: companionPanelSnapshotInt(values, "spirit"),
PhysicalAttack: companionPanelSnapshotInt(values, "physical_attack"),
MagicalAttack: companionPanelSnapshotInt(values, "magical_attack"),
IndependentAttack: companionPanelSnapshotInt(values, "independent_attack"),
PhysicalDefense: companionPanelSnapshotInt(values, "physical_defense"),
MagicalDefense: companionPanelSnapshotInt(values, "magical_defense"),
FireResistance: companionPanelSnapshotInt(values, "fire_resistance"),
WaterResistance: companionPanelSnapshotInt(values, "water_resistance"),
DarkResistance: companionPanelSnapshotInt(values, "dark_resistance"),
LightResistance: companionPanelSnapshotInt(values, "light_resistance"),
MoveSpeed: companionPanelSnapshotInt(values, "move_speed"),
AttackSpeed: companionPanelSnapshotInt(values, "attack_speed"),
CastSpeed: companionPanelSnapshotInt(values, "cast_speed"),
HitRecovery: companionPanelSnapshotInt(values, "hit_recovery"),
JumpPower: companionPanelSnapshotInt(values, "jump_power"),
Weight: companionPanelSnapshotInt(values, "weight"),
InventoryLimit: companionPanelSnapshotInt(values, "inventory_limit"),
HPRegenSpeed: companionPanelSnapshotInt(values, "hp_regen"),
MPRegenSpeed: companionPanelSnapshotInt(values, "mp_regen"),
SourceClient: strings.TrimSpace(values["source_client"]),
PhysicalCritical: companionPanelSnapshotInt(values, "physical_critical"),
MagicalCritical: companionPanelSnapshotInt(values, "magical_critical"),
MagicResistance: companionPanelSnapshotInt(values, "magic_resistance"),
HitRate: companionPanelSnapshotInt(values, "hit_rate"),
DodgeRate: companionPanelSnapshotInt(values, "dodge_rate"),
Stiffness: companionPanelSnapshotInt(values, "stiffness"),
FireDamage: companionPanelSnapshotInt(values, "fire_damage"),
WaterDamage: companionPanelSnapshotInt(values, "water_damage"),
LightDamage: companionPanelSnapshotInt(values, "light_damage"),
DarkDamage: companionPanelSnapshotInt(values, "dark_damage"),
}
if snapshot.HPMax <= 0 || snapshot.MPMax <= 0 || snapshot.Strength <= 0 {
return companionPanelSnapshot{}, false
}
return snapshot, true
}
func (s companionPanelSnapshot) Vector() dnfcharstat.Vector {
return dnfcharstat.Vector{
HPMax: s.HPMax,
MPMax: s.MPMax,
Strength: s.Strength,
Intelligence: s.Intelligence,
Vitality: s.Vitality,
Spirit: s.Spirit,
PhysicalAttack: s.PhysicalAttack,
PhysicalDefense: s.PhysicalDefense,
MagicalAttack: s.MagicalAttack,
MagicalDefense: s.MagicalDefense,
IndependentAttack: s.IndependentAttack,
FireResistance: s.FireResistance,
WaterResistance: s.WaterResistance,
DarkResistance: s.DarkResistance,
LightResistance: s.LightResistance,
InventoryLimit: s.InventoryLimit,
HPRegenSpeed: s.HPRegenSpeed,
MPRegenSpeed: s.MPRegenSpeed,
MoveSpeed: s.MoveSpeed,
AttackSpeed: s.AttackSpeed,
CastSpeed: s.CastSpeed,
HitRecovery: s.HitRecovery,
JumpPower: s.JumpPower,
Weight: s.Weight,
}
}
// CompanionPanelValues returns the raw stored panel snapshot values for the GM
// editor. found is false when no snapshot has been saved yet.
func (s *Service) CompanionPanelValues(ctx context.Context, characterID string) (map[string]string, bool, error) {
repos, ok := s.repositoryGroup()
if !ok {
return nil, false, ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return nil, false, errCompanionPanelCharacterID
}
record, found, err := repos.Settings.Load(ctx, companionPanelSnapshotScope(characterID))
if err != nil {
return nil, false, err
}
if !found {
return map[string]string{}, false, nil
}
values := make(map[string]string, len(record.Values))
nonEmpty := false
for k, v := range record.Values {
values[k] = v
if strings.TrimSpace(v) != "" {
nonEmpty = true
}
}
// A record whose values are all empty is a leftover of a blank GM save and
// carries no usable panel; report it as not configured so the editor does
// not show a misleading "已读取".
if !nonEmpty {
return map[string]string{}, false, nil
}
return values, true, nil
}
// SaveCompanionPanelValues replaces the stored panel snapshot values.
func (s *Service) SaveCompanionPanelValues(ctx context.Context, characterID string, values map[string]string) error {
repos, ok := s.repositoryGroup()
if !ok {
return ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return errCompanionPanelCharacterID
}
cleaned := make(map[string]string, len(values))
for k, v := range values {
key := strings.TrimSpace(k)
if key == "" {
continue
}
cleaned[key] = strings.TrimSpace(v)
}
return repos.Settings.Save(ctx, dnfrepo.SettingsRecord{
Scope: companionPanelSnapshotScope(characterID),
Values: cleaned,
})
}
// GenerateCompanionPanelSnapshot builds a best-effort panel snapshot from the
// server's PVF character growth plus the character's worn equipment PVF stats
// (and weapon forging). It deliberately does not model enchant cards,
// amplification, reinforcement, quality, set effects, titles, auras or pets:
// the saved values are a BASE ESTIMATE that the GM page can correct.
func (s *Service) GenerateCompanionPanelSnapshot(ctx context.Context, characterID string) (map[string]string, error) {
repos, ok := s.repositoryGroup()
if !ok {
return nil, ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return nil, errCompanionPanelCharacterID
}
character, found, err := repos.Character.Load(ctx, characterID)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("character %s not found", characterID)
}
base, ok := s.characterPVFStatsForUserInfo(ctx, nil, character, true)
if !ok {
return nil, fmt.Errorf("character %s PVF stats unavailable", characterID)
}
stats := s.addEquipmentPVFStatsForUserInfo(ctx, nil, character, base)
values := companionPanelValuesFromVector(stats, character)
return values, nil
}
func companionPanelValuesFromVector(stats dnfcharstat.Vector, character dnfrepo.CharacterRecord) map[string]string {
values := make(map[string]string, 24)
put := func(key string, value int64) {
values[key] = strconv.FormatInt(value, 10)
}
put("hp_max", stats.HPMax)
put("mp_max", stats.MPMax)
put("strength", stats.Strength)
put("intelligence", stats.Intelligence)
put("vitality", stats.Vitality)
put("spirit", stats.Spirit)
put("physical_attack", stats.PhysicalAttack)
put("magical_attack", stats.MagicalAttack)
put("independent_attack", stats.IndependentAttack)
put("physical_defense", stats.PhysicalDefense)
put("magical_defense", stats.MagicalDefense)
put("fire_resistance", stats.FireResistance)
put("water_resistance", stats.WaterResistance)
put("dark_resistance", stats.DarkResistance)
put("light_resistance", stats.LightResistance)
put("move_speed", stats.MoveSpeed)
put("attack_speed", stats.AttackSpeed)
put("cast_speed", stats.CastSpeed)
put("hit_recovery", stats.HitRecovery)
jump := stats.JumpPower
if jump == 0 {
jump = character.Stats["stat_jump_power"]
}
put("jump_power", jump)
weight := stats.Weight
if weight == 0 {
weight = character.Stats["stat_weight"]
}
put("weight", weight)
inventory := stats.InventoryLimit
if inventory == 0 {
inventory = character.Stats["stat_inventory_limit"]
}
put("inventory_limit", inventory)
put("hp_regen", stats.HPRegenSpeed)
put("mp_regen", stats.MPRegenSpeed)
values["source_client"] = "server-estimate"
return values
}
@@ -0,0 +1,192 @@
package dnfbridge
import (
"context"
"os"
"strings"
"longheng.io/server/internal/modules/dnf/dnfenum"
)
// Real-actor PoC for the volunteer squad.
//
// This path is deliberately switch-gated and OFF by default. It publishes each
// volunteer as a real character actor (mode0 + mode1 + mode3, objectKey =
// character id, non-zero party owner context) instead of the support-soldier
// APC projection. It is the first milestone of the "real character enters the
// dungeon" route; attaching the client AIC to these actors is the next client
// patch step, so this path currently ships appearance/roster only.
const (
currentCompanionRealActorsEnv = "DNF_COMPANION_REAL_ACTORS"
currentCompanionRealActorsFlagFile = "companion_real_actors.enabled"
currentCompanionRealActorsMode0Kind = "real_party_actor"
)
func currentCompanionRealActorsEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentCompanionRealActorsEnv))) {
case "0", "off", "false", "no":
return false
}
if _, err := os.Stat(currentCompanionRealActorsFlagFile); err == nil {
return true
}
return false
}
// publishCurrentDungeonCompanionRealActors returns true only when every ready
// volunteer was published as a real actor. A partial failure returns false so
// the caller can keep the proven projection path instead of showing a broken
// half-team.
func (s *Service) publishCurrentDungeonCompanionRealActors(
session *gameSession,
runtime *runtimeDungeonState,
) bool {
if s == nil || session == nil || runtime == nil || runtime.Companion == nil ||
!currentCompanionRealActorsEnabled() {
return false
}
run := runtime.Companion
if !run.Ready || len(run.Sources) == 0 {
return false
}
ownerContext := currentPartySceneOwnerContext(session)
if ownerContext == 0 && session.channel.ID > 0 && session.channel.ID <= 0xff {
ownerContext = byte(session.channel.ID)
}
if ownerContext == 0 {
s.logGameEvent(session, "game-companion-real-actors-skipped",
"owner_character_id", session.selectedCharacterID,
"reason", "party_owner_context_not_committed")
return false
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
type realRow struct {
id uint16
name string
mode0 []byte
mode1 []byte
}
rows := make([]realRow, 0, len(run.Sources))
for _, entry := range run.Sources {
if !entry.Ready || entry.SourceCharacterID == 0 {
continue
}
state := entry.ProgressionSnapshot
if state.Source.CharacterID == "" {
return false
}
name := strings.TrimSpace(state.Source.Name)
if name == "" {
name = entry.SourceName
}
mode0, err := s.buildCurrentSceneObjectListBodyForSessionInContextStrict(
ctx, session, entry.SourceCharacterID, name, state.Source, true, ownerContext)
if err != nil || len(mode0) < 3 {
s.logGameEvent(session, "game-companion-real-actor-failed",
"source_character_id", entry.SourceCharacterID,
"stage", "mode0", "error", err)
return false
}
reader := csharpLegacyUserInfoReader{
ctx: ctx,
characterID: state.Source.CharacterID,
service: s,
session: session,
suppressActorSkillResources: true,
equipmentSnapshot: &state.Equipment,
}
mode1 := reader.buildCurrentUserInfoMode1WithEquipmentForOwner(
state.Source, true, entry.SourceCharacterID, true, 0,
currentSceneObjectRoute, ownerContext)
if len(mode1) < 3 {
s.logGameEvent(session, "game-companion-real-actor-failed",
"source_character_id", entry.SourceCharacterID,
"stage", "mode1")
return false
}
rows = append(rows, realRow{id: entry.SourceCharacterID, name: name, mode0: mode0, mode1: mode1})
}
if len(rows) == 0 {
return false
}
// The client's native multi-row reader creates scene actors for a party
// prelude. Sending one row at a time produced global visuals that a dungeon
// room never rendered; the roster must be one counted mode0/mode1 body.
var multiMode0, multiMode1 packetWriter
multiMode0.writeByte(0)
multiMode0.writeUint16(uint16(len(rows)))
multiMode1.writeByte(1)
multiMode1.writeUint16(uint16(len(rows)))
for _, row := range rows {
multiMode0.writeBytes(row.mode0[3:])
multiMode1.writeBytes(row.mode1[3:])
}
for _, body := range [][]byte{multiMode0.bytes(), multiMode1.bytes()} {
if err := s.sendGameUpperRawClass(
session, uint16(dnfenum.CmdPacketSetUDPIPPort), body, 0); err != nil {
s.logGameEvent(session, "game-companion-real-actor-failed",
"stage", "multi-row-send", "error", err)
return false
}
}
session.townMu.Lock()
position := session.townPositionSnapshot
session.townMu.Unlock()
stateBits := currentActorDefaultUserStateBits
if runtime.Character.Stats != nil {
if value, exists := runtime.Character.Stats["user_state_bits"]; exists && value >= 0 && value <= 0xff {
stateBits = byte(value)
}
}
for index, row := range rows {
x := int(position.PositionX) + index*40
y := int(position.PositionY)
if !position.PositionValid {
x = int(numericCharacterStatValue(runtime.Character, "pos_x")) + index*40
y = int(numericCharacterStatValue(runtime.Character, "pos_y"))
}
if x < 0 {
x = 0
}
if x > 0xffff {
x = 0xffff
}
if y < 0 {
y = 0
}
if y > 0xffff {
y = 0xffff
}
areaBody := buildCurrentTownUserAreaNotificationBody(
currentSceneActorObjectKey(row.id),
position.TownID, 0xff, uint16(x), uint16(y),
position.MovementCode, stateBits,
)
if err := s.sendGameUpperRawClass(
session, currentTownUserAreaNotificationMsgID, areaBody, 0); err != nil {
s.logGameEvent(session, "game-companion-real-actor-failed",
"source_character_id", row.id,
"stage", "op23", "error", err)
return false
}
s.logGameEvent(session, "game-companion-real-actor-sent",
"source_character_id", row.id,
"name", row.name,
"owner_context", ownerContext,
"object_key", currentSceneActorObjectKey(row.id),
"mode0_bytes", len(row.mode0),
"mode1_bytes", len(row.mode1),
"path", currentCompanionRealActorsMode0Kind)
}
sent := len(rows)
if sent == 0 {
return false
}
s.logGameEvent(session, "game-companion-real-actors-published",
"owner_character_id", session.selectedCharacterID,
"count", sent,
"party_owner_context", ownerContext,
"aic", "pending_client_attachment")
return true
}
@@ -0,0 +1,268 @@
package dnfbridge
import (
"context"
"encoding/binary"
"os"
"strconv"
"strings"
"time"
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// The teammate squad is the default support-soldier entry shape. It can be
// disabled per deployment with DNF_COMPANION_TEAM_MODE=0; the GM settings stay
// authoritative for which characters are preferred, but the server fills the
// remaining slots from the same account at dungeon entry.
const currentDungeonCompanionTeamModeEnv = "DNF_COMPANION_TEAM_MODE"
const currentDungeonCompanionTeamRole = "teammate"
func currentDungeonCompanionTeamModeEnabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentDungeonCompanionTeamModeEnv))) {
case "0", "off", "false", "no":
return false
default:
return true
}
}
// autoMatchCurrentDungeonCompanionSources expands the operator's saved
// selection into a frozen teammate roster for this dungeon.
//
// This is a read-only matchmaking step: no source character is modified and no
// session is created for it. The actual snapshot/AIC/projection pipeline still
// runs per selected source exactly as before, so a bad or deleted character
// fails on its own row instead of aborting the whole team.
func (s *Service) autoMatchCurrentDungeonCompanionSources(
ctx context.Context,
repos dnfrepo.Group,
accountID string,
ownerCharacterID uint16,
configured []string,
) ([]string, []dnfcompanion.TeamSkip, bool) {
if s == nil || repos.Character == nil || strings.TrimSpace(accountID) == "" ||
!currentDungeonCompanionTeamModeEnabled() {
return configured, nil, false
}
records, err := repos.Character.ListByAccount(ctx, accountID, dnfrepo.MaxCharacterSlotLimit)
if err != nil {
s.logPacketEvent("dnf-companion-team-match-deferred",
"owner_character_id", ownerCharacterID,
"reason", "character_list_unavailable",
"error", err)
return configured, nil, false
}
ownerLevel := 0
if owner, ownerFound, ownerErr := repos.Character.Load(ctx, strconv.FormatUint(uint64(ownerCharacterID), 10)); ownerErr == nil && ownerFound {
ownerLevel = owner.Level
}
levelFloor := ownerLevel - 10
if levelFloor < 1 {
levelFloor = 1
}
candidates := make([]dnfcompanion.TeamCandidate, 0, len(records))
for _, record := range records {
id := numericCharacterID(record)
if id <= 0 || id >= int(^uint16(0)) {
continue
}
if ownerLevel > 0 && record.Level > 0 && record.Level < levelFloor {
// A level-21 character in a level-90 party's dungeon entry preflight
// makes the whole party fail the level gate. Do not auto-fill with a
// character the dungeon will reject.
s.logPacketEvent("dnf-companion-team-match-level-skipped",
"owner_character_id", ownerCharacterID,
"candidate_character_id", id,
"candidate_level", record.Level,
"owner_level", ownerLevel,
"level_floor", levelFloor)
continue
}
session, online := s.onlineGameSession(uint16(id))
if online && session != nil && session.companionHeadless {
// Our own headless presence is not a competing real player. Without
// this, every later auto-match (preflight/dungeon entry) excludes
// the volunteers it just logged in.
online = false
}
candidates = append(candidates, dnfcompanion.TeamCandidate{
CharacterID: strconv.Itoa(id),
Level: record.Level,
Job: strings.TrimSpace(record.Job),
Slot: record.Slot,
Online: online,
Deleted: record.Stats["delete_flag"] != 0,
})
}
match := dnfcompanion.MatchTeam(
strconv.FormatUint(uint64(ownerCharacterID), 10),
configured,
candidates,
dnfcompanion.TeamMaxCompanions,
)
skipped := make([]string, 0, len(match.Skipped))
for _, entry := range match.Skipped {
if len(skipped) >= 16 {
break
}
skipped = append(skipped, entry.CharacterID+":"+entry.Reason)
}
s.logPacketEvent("dnf-companion-team-match",
"owner_character_id", ownerCharacterID,
"configured", strings.Join(configured, ","),
"selected", strings.Join(match.Selected, ","),
"selected_count", len(match.Selected),
"skipped", strings.Join(skipped, ","),
"limit", dnfcompanion.TeamMaxCompanions)
return match.Selected, match.Skipped, true
}
func currentDungeonCompanionTeamSourceIDs(run *currentDungeonCompanionRun) []uint16 {
if run == nil {
return nil
}
out := make([]uint16, 0, len(run.Sources))
for _, entry := range run.Sources {
if entry.Ready {
out = append(out, entry.SourceCharacterID)
}
}
if len(out) == 0 && run.Ready && run.SourceCharacterID != 0 {
out = append(out, run.SourceCharacterID)
}
return out
}
func currentDungeonCompanionTeamRevision(run *currentDungeonCompanionRun) uint64 {
if run == nil {
return 1
}
if len(run.Body) >= currentDungeonCompanionProjectionHeaderSize {
if revision := binary.LittleEndian.Uint64(run.Body[8:16]); revision != 0 {
return revision
}
}
for _, entry := range run.Sources {
if len(entry.Body) >= currentDungeonCompanionProjectionHeaderSize {
if revision := binary.LittleEndian.Uint64(entry.Body[8:16]); revision != 0 {
return revision
}
}
}
return 1
}
func (s *Service) markCurrentDungeonCompanionTeamOnline(
session *gameSession,
run *currentDungeonCompanionRun,
source string,
) {
if s == nil || run == nil || !run.Ready {
return
}
run.Online = true
run.OnlineAt = s.gameplayNow()
run.OfflineAt = time.Time{}
run.OfflineReason = ""
run.LogoutSent = false
s.logGameEvent(session, "game-dungeon-companion-team-online",
"source", source,
"team_mode", run.TeamMode,
"team_leader_id", run.TeamLeaderID,
"team_size", run.TeamSize,
"ready_count", currentDungeonCompanionReadyCount(run),
"source_character_ids", uint16ListLog(currentDungeonCompanionTeamSourceIDs(run)),
"online_at", run.OnlineAt)
}
// sendCurrentDungeonCompanionTeamOffline publishes the explicit present=0
// projection. The client clears every queued/live support actor on that body,
// so one packet is enough for the whole squad and it is safe to call from every
// dungeon boundary: the run's LogoutSent flag makes it idempotent.
func (s *Service) sendCurrentDungeonCompanionTeamOffline(
session *gameSession,
runtime *runtimeDungeonState,
reason string,
) {
if s == nil || session == nil || runtime == nil {
return
}
run := runtime.Companion
if run == nil || !run.Ready || run.LogoutSent {
return
}
run.LogoutSent = true
run.Online = false
run.OfflineAt = s.gameplayNow()
run.OfflineReason = strings.TrimSpace(reason)
// The dungeon party frame goes away; the synthetic friend sessions do not.
s.clearCurrentDungeonCompanionNativeParty(session, run)
ids := currentDungeonCompanionTeamSourceIDs(run)
body := buildCurrentDungeonCompanionOfflineBody(currentDungeonCompanionTeamRevision(run))
s.logGameEvent(session, "game-dungeon-companion-team-offline",
"reason", run.OfflineReason,
"team_mode", run.TeamMode,
"team_leader_id", run.TeamLeaderID,
"team_size", run.TeamSize,
"offline_count", len(ids),
"source_character_ids", uint16ListLog(ids),
"offline_at", run.OfflineAt,
"private_opcode", currentDungeonCompanionProjectionOpcode,
"body_len", len(body))
if err := s.sendGameUpperRawClass(
session,
currentDungeonCompanionProjectionOpcode,
body,
0,
); err != nil {
s.logGameEvent(session, "game-dungeon-companion-team-offline-send-failed",
"reason", run.OfflineReason,
"error", err)
}
}
func (s *Service) sendCurrentDungeonCompanionTeamOfflineCurrent(
session *gameSession,
reason string,
) {
if s == nil || session == nil {
return
}
session.dungeon.mu.Lock()
runtime := session.dungeon.runtime
session.dungeon.mu.Unlock()
s.sendCurrentDungeonCompanionTeamOffline(session, runtime, reason)
}
func uint16ListLog(values []uint16) string {
if len(values) == 0 {
return ""
}
var builder strings.Builder
for index, value := range values {
if index != 0 {
builder.WriteByte(',')
}
builder.WriteString(strconv.Itoa(int(value)))
}
return builder.String()
}
// runtimeDungeonCompanionCurrent is the compare step used by exit paths that
// opportunistically publish the logout before taking session.dungeon.mu. The
// final authority remains retireCurrentDungeonRuntimeLocked; this only avoids
// sending a stale run's packet when a newer dungeon already replaced it.
func (s *Service) runtimeDungeonCompanionCurrent(
session *gameSession,
runtime *runtimeDungeonState,
) bool {
if s == nil || session == nil || runtime == nil {
return false
}
session.dungeon.mu.Lock()
defer session.dungeon.mu.Unlock()
return session.dungeon.runtime == runtime
}
@@ -0,0 +1,111 @@
package dnfbridge
import (
"bytes"
"encoding/binary"
"net"
"testing"
"time"
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
)
// companionOfflineTestConn is intentionally local to this test so the file can
// be compiled without the large shared service_test.go harness.
type companionOfflineTestConn struct {
net.Conn
write bytes.Buffer
}
func (c *companionOfflineTestConn) Write(data []byte) (int, error) { return c.write.Write(data) }
func (c *companionOfflineTestConn) SetWriteDeadline(time.Time) error {
return nil
}
func TestBuildCurrentDungeonCompanionOfflineBodyMatchesPresentZeroContract(t *testing.T) {
body := buildCurrentDungeonCompanionOfflineBody(12345)
if len(body) != currentDungeonCompanionProjectionHeaderSize {
t.Fatalf("body length=%d want=%d", len(body), currentDungeonCompanionProjectionHeaderSize)
}
if got := binary.LittleEndian.Uint32(body[0:4]); got != currentDungeonCompanionProjectionMagic {
t.Fatalf("magic=%#x want=%#x", got, currentDungeonCompanionProjectionMagic)
}
if body[4] != currentDungeonCompanionProjectionVersion {
t.Fatalf("version=%d want=%d", body[4], currentDungeonCompanionProjectionVersion)
}
if body[5] != 0 {
t.Fatalf("flags=%#x want 0 (present must be clear)", body[5])
}
if got := binary.LittleEndian.Uint16(body[6:8]); got != 0 {
t.Fatalf("source_character=%d want 0", got)
}
if got := binary.LittleEndian.Uint64(body[8:16]); got != 12345 {
t.Fatalf("revision=%d want 12345", got)
}
for offset := 16; offset < len(body); offset++ {
if body[offset] != 0 {
t.Fatalf("byte %d=%#x want 0", offset, body[offset])
}
}
}
func TestSendCurrentDungeonCompanionTeamOfflineIsIdempotent(t *testing.T) {
connection := &companionOfflineTestConn{}
session := &gameSession{conn: connection, connID: "companion-team-logout"}
run := &currentDungeonCompanionRun{
Ready: true,
TeamMode: true,
TeamLeaderID: 1,
TeamSize: 2,
Online: true,
Sources: []currentDungeonCompanionSourceRun{
{Ready: true, SourceCharacterID: 2, Body: buildCurrentDungeonCompanionOfflineBody(77)},
{Ready: true, SourceCharacterID: 3, Body: buildCurrentDungeonCompanionOfflineBody(77)},
},
}
runtime := &runtimeDungeonState{Companion: run}
service := &Service{}
service.sendCurrentDungeonCompanionTeamOffline(session, runtime, "dungeon_return_town")
if !run.LogoutSent || run.Online {
t.Fatalf("run state after logout = sent:%v online:%v", run.LogoutSent, run.Online)
}
if run.OfflineAt.IsZero() || run.OfflineReason != "dungeon_return_town" {
t.Fatalf("offline evidence = at:%v reason:%q", run.OfflineAt, run.OfflineReason)
}
first := connection.write.Len()
if first == 0 {
t.Fatal("logout packet was not sent")
}
// A second boundary must not replay the packet.
service.sendCurrentDungeonCompanionTeamOffline(session, runtime, "return_select_character")
if connection.write.Len() != first {
t.Fatalf("second logout wrote %d extra bytes", connection.write.Len()-first)
}
stream := connection.write.Bytes()
headerSize := service.gameUpperHeaderSize()
if len(stream) < headerSize {
t.Fatalf("logout stream too short: %d", len(stream))
}
total := int(binary.LittleEndian.Uint32(stream[3:7]))
if total != len(stream) {
t.Fatalf("logout packet total=%d stream=%d", total, len(stream))
}
if msgID := binary.LittleEndian.Uint16(stream[1:3]); msgID != currentDungeonCompanionProjectionOpcode {
t.Fatalf("msg id=%#x want=%#x", msgID, currentDungeonCompanionProjectionOpcode)
}
body := stream[headerSize:total]
if len(body) != currentDungeonCompanionProjectionHeaderSize || body[5] != 0 {
t.Fatalf("logout body length=%d flags=%d", len(body), body[5])
}
}
func TestMatchTeamAutoFillKeepsProjectionPipelinePerSource(t *testing.T) {
// The matcher is pure; this guard pins the cap the bridge passes so a future
// change cannot silently form a five-player dungeon party.
if dnfcompanion.TeamMaxCompanions != 3 {
t.Fatalf("TeamMaxCompanions=%d want 3", dnfcompanion.TeamMaxCompanions)
}
}
@@ -45,6 +45,20 @@ type gameSession struct {
completedAutoNextRuntime *runtimeDungeonState
conn net.Conn
// companionVirtual marks a synthetic session that exists only so the native
// friend list and party frame can resolve an offline volunteer as an online
// character. It has no socket and must never receive a normal send path.
companionVirtual bool
// companionHeadless is a real session in gameSessions with a sink transport.
// Party/dungeon owners treat it as a real member; no client is attached.
companionHeadless bool
companionOwnerID uint16
// companionForceEnter marks the admin-driven op16 so dungeon entry can log
// its exact refusal reason to the plaintext headless debug file.
companionForceEnter bool
// headlessActorPublished marks that this headless member's own actor rows
// were projected to the leader after the dungeon room committed.
headlessActorPublished bool
// writer owns this session's blocking socket writes so the event loop never
// parks on a client that stopped reading. See game_session_writer.go for the
// live 48-second stall measurement that made it necessary. nil means "no
@@ -13,6 +13,10 @@ type boundGameSessionCharacter struct {
session *gameSession
character uint16
generation uint64
// virtual marks a companion presence session. Such a session is not in
// Service.gameSessions (it has no socket), so the liveness check below must
// consult Service.companionSessions instead.
virtual bool
}
func (s *Service) bindGameSessionCharacter(session *gameSession, charID uint16) {
@@ -53,6 +57,13 @@ func (s *Service) bindGameSessionCharacter(session *gameSession, charID uint16)
s.stopCurrentSpendTimeClock(session, "selected_character_changed")
s.stopCurrentPetGrowthClock(session, "selected_character_changed")
}
if oldID := session.selectedCharacterID; oldID != 0 && oldID != charID {
session.dungeon.mu.Lock()
pendingCompanionRuntime := session.dungeon.runtime
session.dungeon.mu.Unlock()
s.sendCurrentDungeonCompanionTeamOffline(
session, pendingCompanionRuntime, "selected_character_changed")
}
session.dungeon.mu.Lock()
if oldID := session.selectedCharacterID; oldID != 0 && oldID != charID {
runtime := session.dungeon.runtime
@@ -143,6 +154,9 @@ func (s *Service) bindGameSessionCharacter(session *gameSession, charID uint16)
session.collectboxSelectionRefreshSent = false
}
session.selectedCharacterID = charID
// A real client binding this character always retires the synthetic
// volunteer presence row, so the friend list cannot claim two online copies.
s.dropCompanionPresenceCharacter(charID)
if resident != nil && resident != session {
if current := s.gameSessions[charID]; current == resident {
s.mu.Unlock()
@@ -436,6 +450,7 @@ func (s *Service) releaseDisconnectedDungeonAttempt(session *gameSession, reason
// 掉线不退队 —— 座位由 raid_disconnect.go 的宽限窗口守着,这里存的坐标只作
// 诊断与重连参考(重连实际一律回 PVF 等待室起点,见 runtimeFiendWarRejoinLocation)。
s.captureRuntimeFiendWarRejoinPosition(session)
s.sendCurrentDungeonCompanionTeamOfflineCurrent(session, "disconnected")
session.dungeon.mu.Lock()
runtime := session.dungeon.runtime
if runtime == nil {
@@ -538,6 +553,9 @@ func (s *Service) boundGameSessionCharacterCurrent(identity boundGameSessionChar
if s == nil || identity.session == nil || identity.character == 0 {
return false
}
if identity.virtual {
return s.companionSessionCurrent(identity)
}
s.mu.Lock()
defer s.mu.Unlock()
return s.gameSessions[identity.character] == identity.session &&
@@ -73,6 +73,7 @@ func (s *Service) commitCurrentAvatarSocketOpen(session *gameSession, request cu
if err != nil {
return currentSocketMutationResult{}, err
}
socketTypes = currentAvatarSocketTypesForOpen(rule, socketTypes)
if len(socketTypes) == 0 {
return currentSocketMutationResult{}, fmt.Errorf("%w: avatar socket definitions missing item=%d", errCurrentSocketPVFInvalid, request.TargetItemID)
}
@@ -0,0 +1,434 @@
package dnfbridge
import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
"time"
dnfauction "longheng.io/server/internal/modules/dnf/auction"
)
type currentServerFeatureSettings struct {
RaidReviveSeconds int `json:"raidReviveSeconds"`
AllDeadReturnSeconds int `json:"allDeadReturnSeconds"`
AllowFreeRevive *bool `json:"allowFreeRevive"`
HellOrbChance int `json:"hellOrbChance"`
HellDropChanceDenominator int `json:"hellDropChanceDenominator"`
EquipmentRollMultiplier int `json:"equipmentRollMultiplier"`
EpicPieceRollMultiplier int `json:"epicPieceRollMultiplier"`
AntonRaidAttackSeconds int `json:"antonRaidAttackSeconds"`
LukeEnergyRecoveryPercent int `json:"lukeEnergyRecoveryPercent"`
FiendWarSquadCount int `json:"fiendWarSquadCount"`
RoomFatigueCost int `json:"roomFatigueCost"`
DailyFatigueResetHour int `json:"dailyFatigueResetHour"`
AdventureGroupBonusPercent int `json:"adventureGroupBonusPercent"`
RentalMaxReinforce int `json:"rentalMaxReinforce"`
RentalMaxForge int `json:"rentalMaxForge"`
ReviveByDungeon map[string]int `json:"reviveByDungeon"`
AntonBlackFogRecoverySeconds int `json:"antonBlackFogRecoverySeconds"`
AntonBlackFogActiveSeconds int `json:"antonBlackFogActiveSeconds"`
AntonQuakeRecoverySeconds int `json:"antonQuakeRecoverySeconds"`
AntonQuakeActiveSeconds int `json:"antonQuakeActiveSeconds"`
AntonNavalOpenDelaySeconds int `json:"antonNavalOpenDelaySeconds"`
AntonNavalRecoverySeconds int `json:"antonNavalRecoverySeconds"`
AntonNavalActiveSeconds int `json:"antonNavalActiveSeconds"`
LukeEnergyRecovery0Seconds int `json:"lukeEnergyRecovery0Seconds"`
LukeEnergyRecovery1Seconds int `json:"lukeEnergyRecovery1Seconds"`
LukeEnergyRecovery2Seconds int `json:"lukeEnergyRecovery2Seconds"`
LukePhaseTimeoutPercent int `json:"lukePhaseTimeoutPercent"`
FiendWarAreaLimitPercent int `json:"fiendWarAreaLimitPercent"`
FiendWarDieImprisonPercent int `json:"fiendWarDieImprisonPercent"`
SoloTeleportCooldownSeconds int `json:"soloTeleportCooldownSeconds"`
SoloTeleportCooldownIndices string `json:"soloTeleportCooldownIndices"`
AuctionStandardListingLimit int `json:"auctionStandardListingLimit"`
AuctionPremiumListingLimit int `json:"auctionPremiumListingLimit"`
AuctionStandardDurationHours int `json:"auctionStandardDurationHours"`
AuctionPremiumDurationHours int `json:"auctionPremiumDurationHours"`
AntonMemberMax int `json:"antonMemberMax"`
AntonStartMinimumMembers int `json:"antonStartMinimumMembers"`
AntonSingleStartMinimumMembers int `json:"antonSingleStartMinimumMembers"`
AntonStartDelaySeconds int `json:"antonStartDelaySeconds"`
AntonPhaseBreakSeconds int `json:"antonPhaseBreakSeconds"`
AntonEntryCostItem int `json:"antonEntryCostItem"`
AntonEntryCostCount int `json:"antonEntryCostCount"`
AntonWeeklyEnterCountA int `json:"antonWeeklyEnterCountA"`
AntonWeeklyEnterCountB int `json:"antonWeeklyEnterCountB"`
AntonPhaseTimeoutSeconds string `json:"antonPhaseTimeoutSeconds"`
LukeDailyAccountEnterLimit int `json:"lukeDailyAccountEnterLimit"`
LukeSanctumTimerCount int `json:"lukeSanctumTimerCount"`
LukeEntryCostItem int `json:"lukeEntryCostItem"`
LukeEntryCostCount int `json:"lukeEntryCostCount"`
FiendWarMemberMax int `json:"fiendWarMemberMax"`
FiendWarStartMinimumMembers int `json:"fiendWarStartMinimumMembers"`
FiendWarDisconnectWaitSeconds int `json:"fiendWarDisconnectWaitSeconds"`
FiendWarEntryCostItem int `json:"fiendWarEntryCostItem"`
FiendWarEntryCostCount int `json:"fiendWarEntryCostCount"`
FiendWarRetry *bool `json:"fiendWarRetry"`
FiendWarDailyEnterLimit int `json:"fiendWarDailyEnterLimit"`
AuctionStandardFeePercent int `json:"auctionStandardFeePercent"`
AuctionPremiumFeePercent int `json:"auctionPremiumFeePercent"`
}
func GMSaveServerFeatures(data []byte) error {
var values map[string]json.RawMessage
if err := json.Unmarshal(data, &values); err != nil {
return err
}
path := currentServerConfigPath("server-features.json")
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
return os.Rename(tmp, path)
}
func currentServerConfigPath(name string) string {
candidates := []string{filepath.Join("config", name)}
if executable, err := os.Executable(); err == nil {
candidates = append(candidates,
filepath.Join(filepath.Dir(executable), "..", "config", name),
filepath.Join(filepath.Dir(executable), "config", name),
)
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
return candidates[0]
}
func currentServerFeatures() currentServerFeatureSettings {
data, err := os.ReadFile(currentServerConfigPath("server-features.json"))
if err != nil {
return currentServerFeatureSettings{}
}
var settings currentServerFeatureSettings
if err := json.Unmarshal(data, &settings); err != nil {
return currentServerFeatureSettings{}
}
return settings
}
func currentActivityEnabled(key string) bool {
data, err := os.ReadFile(currentServerConfigPath("activity.json"))
if err != nil {
return true
}
var values map[string]bool
if err := json.Unmarshal(data, &values); err != nil {
return true
}
enabled, found := values[key]
if !found {
return true
}
return enabled
}
func currentConfiguredAllDeadReturnDelay() time.Duration {
const fallback = currentPartyDungeonAllDeadReturnDelay
settings := currentServerFeatures()
if settings.AllDeadReturnSeconds < 1 || settings.AllDeadReturnSeconds > 60 {
return fallback
}
return time.Duration(settings.AllDeadReturnSeconds) * time.Second
}
func currentConfiguredAllowFreeRevive() bool {
settings := currentServerFeatures()
if settings.AllowFreeRevive == nil {
return currentDungeonUseCoinAllowFreeRevive
}
return *settings.AllowFreeRevive
}
func currentConfiguredHellOrbChance() int {
settings := currentServerFeatures()
if settings.HellOrbChance < 0 || settings.HellOrbChance > currentDungeonHellOrbRollDenominator {
return currentDungeonHellOrbChance
}
return settings.HellOrbChance
}
func currentConfiguredHellDropChanceDenominator() int {
settings := currentServerFeatures()
if settings.HellDropChanceDenominator < 1 || settings.HellDropChanceDenominator > 1_000_000 {
return currentHellPartyDropChanceDenominator
}
return settings.HellDropChanceDenominator
}
func currentConfiguredAntonRaidAttackSeconds() int {
settings := currentServerFeatures()
if settings.AntonRaidAttackSeconds < 60 || settings.AntonRaidAttackSeconds > 24*60*60 {
return antonRaidAttackSeconds
}
return settings.AntonRaidAttackSeconds
}
func currentConfiguredLukeEnergyRecoveryPercent() int {
settings := currentServerFeatures()
if settings.LukeEnergyRecoveryPercent < 1 || settings.LukeEnergyRecoveryPercent > 1000 {
return 100
}
return settings.LukeEnergyRecoveryPercent
}
func currentConfiguredFiendWarSquadCount() int {
settings := currentServerFeatures()
if settings.FiendWarSquadCount < 1 || settings.FiendWarSquadCount > 8 {
return currentFiendWarSquadCount
}
return settings.FiendWarSquadCount
}
func currentConfiguredRoomFatigueCost() int64 {
settings := currentServerFeatures()
if settings.RoomFatigueCost < 0 || settings.RoomFatigueCost > 100 {
return currentDungeonRoomFatigueCost
}
return int64(settings.RoomFatigueCost)
}
func currentConfiguredDailyFatigueResetHour() int {
settings := currentServerFeatures()
if settings.DailyFatigueResetHour < 0 || settings.DailyFatigueResetHour > 23 {
return 6
}
return settings.DailyFatigueResetHour
}
func currentConfiguredAdventureGroupBonusPercent() int {
settings := currentServerFeatures()
if settings.AdventureGroupBonusPercent < 0 || settings.AdventureGroupBonusPercent > 1000 {
return currentDungeonAdventureGroupDefaultClearBonusPercent
}
return settings.AdventureGroupBonusPercent
}
func currentConfiguredRentalMaxReinforce() int {
settings := currentServerFeatures()
if settings.RentalMaxReinforce < 0 || settings.RentalMaxReinforce > 31 {
return currentRentalMaxReinforce
}
return settings.RentalMaxReinforce
}
func currentConfiguredRentalMaxForge() int {
settings := currentServerFeatures()
if settings.RentalMaxForge < 0 || settings.RentalMaxForge > 255 {
return currentRentalMaxForge
}
return settings.RentalMaxForge
}
func currentConfiguredDungeonReviveSeconds(dungeonID int64) (int, bool) {
if dungeonID <= 0 {
return 0, false
}
settings := currentServerFeatures()
if settings.ReviveByDungeon == nil {
return 0, false
}
value, found := settings.ReviveByDungeon[strconv.FormatInt(dungeonID, 10)]
if !found || value < 1 || value > 86400 {
return 0, false
}
return value, true
}
func currentConfiguredDungeonReviveDelay(dungeonID int64) time.Duration {
seconds, found := currentConfiguredDungeonReviveSeconds(dungeonID)
if !found {
return currentConfiguredRaidReviveDelay()
}
return time.Duration(seconds) * time.Second
}
func currentAntonDuration(configured int, fallback time.Duration) time.Duration {
if configured < 1 || configured > 86400 {
return fallback
}
return time.Duration(configured) * time.Second
}
func currentAntonBlackFogRecovery() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonBlackFogRecoverySeconds, antonBlackFogRecovery)
}
func currentAntonBlackFogActive() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonBlackFogActiveSeconds, antonBlackFogActive)
}
func currentAntonQuakeRecovery() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonQuakeRecoverySeconds, antonQuakeRecovery)
}
func currentAntonQuakeActive() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonQuakeActiveSeconds, antonQuakeActive)
}
func currentAntonNavalOpenDelay() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonNavalOpenDelaySeconds, antonNavalOpenDelay)
}
func currentAntonNavalRecovery() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonNavalRecoverySeconds, antonNavalRecovery)
}
func currentAntonNavalActive() time.Duration {
return currentAntonDuration(currentServerFeatures().AntonNavalActiveSeconds, antonNavalActive)
}
func currentLukeEnergyRecovery(tier int) time.Duration {
if tier < 0 {
tier = 0
}
if tier >= len(lukeEnergyControlRecovery) {
tier = len(lukeEnergyControlRecovery) - 1
}
settings := currentServerFeatures()
values := [...]int{
settings.LukeEnergyRecovery0Seconds,
settings.LukeEnergyRecovery1Seconds,
settings.LukeEnergyRecovery2Seconds,
}
if values[tier] >= 1 && values[tier] <= 86400 {
return time.Duration(values[tier]) * time.Second
}
percent := settings.LukeEnergyRecoveryPercent
if percent < 1 || percent > 1000 {
percent = 100
}
return time.Duration(int64(lukeEnergyControlRecovery[tier]) * int64(percent) / 100)
}
func currentLukePhaseTimeoutPercent() int {
percent := currentServerFeatures().LukePhaseTimeoutPercent
if percent < 1 || percent > 1000 {
return 100
}
return percent
}
func currentFiendWarAreaLimitPercent() int {
percent := currentServerFeatures().FiendWarAreaLimitPercent
if percent < 1 || percent > 1000 {
return 100
}
return percent
}
func currentFiendWarDieImprisonPercent() int {
percent := currentServerFeatures().FiendWarDieImprisonPercent
if percent < 1 || percent > 1000 {
return 100
}
return percent
}
func currentConfiguredSoloTeleportCooldownSeconds() uint32 {
seconds := currentServerFeatures().SoloTeleportCooldownSeconds
if seconds < 1 || seconds > 86400 {
return currentSoloTeleportCooldownSeconds
}
return uint32(seconds)
}
func currentConfiguredSoloTeleportIndices() []byte {
raw := strings.TrimSpace(currentServerFeatures().SoloTeleportCooldownIndices)
if raw == "" {
return nil
}
if strings.EqualFold(raw, "off") || strings.EqualFold(raw, "none") {
return []byte{}
}
var indices []byte
for _, field := range strings.Split(raw, ",") {
value, err := strconv.ParseUint(strings.TrimSpace(field), 10, 8)
if err == nil && currentSoloTeleportCooldownIndexAccepted(byte(value)) {
indices = append(indices, byte(value))
}
}
return indices
}
func currentConfiguredAuctionListingLimit(premium bool) int {
settings := currentServerFeatures()
value := settings.AuctionStandardListingLimit
if premium {
value = settings.AuctionPremiumListingLimit
}
if value < 1 || value > 1000 {
if premium {
return currentAuctionPremiumListingLimit
}
return currentAuctionStandardListingLimit
}
return value
}
func currentConfiguredAuctionDuration(premium bool) time.Duration {
settings := currentServerFeatures()
hours := settings.AuctionStandardDurationHours
if premium {
hours = settings.AuctionPremiumDurationHours
}
if hours < 1 || hours > 720 {
if premium {
return currentAuctionPremiumDuration
}
return currentAuctionStandardDuration
}
return time.Duration(hours) * time.Hour
}
func currentConfiguredAuctionFeePercent(premium bool) int64 {
settings := currentServerFeatures()
value := settings.AuctionStandardFeePercent
if premium {
value = settings.AuctionPremiumFeePercent
}
if value <= 0 || value > 100 {
return dnfauction.SaleFeePercentFor(premium)
}
return int64(value)
}
// currentConfiguredRaidReviveDelay reads the optional runtime/config/
// server-features.json written by the standalone server tool. It only changes
// the server-side death watchdog; the current EXE still owns its ten-second
// revive prompt.
func currentConfiguredRaidReviveDelay() time.Duration {
const fallback = currentDungeonDeathReturnDelay
settings := currentServerFeatures()
if settings.RaidReviveSeconds < 1 || settings.RaidReviveSeconds > 60 {
return fallback
}
return time.Duration(settings.RaidReviveSeconds) * time.Second
}
// currentConfiguredEquipmentRollMultiplier keeps the local test-end special
// drop tuning (2x equipment rolls) configurable by the standalone server tool.
func currentConfiguredEquipmentRollMultiplier() int {
settings := currentServerFeatures()
if settings.EquipmentRollMultiplier < 1 || settings.EquipmentRollMultiplier > 100 {
return 1
}
return settings.EquipmentRollMultiplier
}
// currentConfiguredEpicPieceRollMultiplier keeps the local test-end special
// drop tuning (3x epic fragment rolls) configurable by the standalone tool.
func currentConfiguredEpicPieceRollMultiplier() int {
settings := currentServerFeatures()
if settings.EpicPieceRollMultiplier < 1 || settings.EpicPieceRollMultiplier > 100 {
return 1
}
return settings.EpicPieceRollMultiplier
}
@@ -85,6 +85,12 @@ type Service struct {
// 配置按账号落库(repos.Settings),运行态按副本冻结;这把锁只保护那一段
// "读当前 revision → 校验 → 写回"的临界区,避免并发保存把 revision 撞掉。
companionConfigMu sync.Mutex
// companionPresenceMu guards the synthetic sessions that expose a volunteer
// in the native friend list / party frame. These sessions have no socket and
// are deliberately NOT stored in gameSessions: only the social and party
// projection helpers below may resolve them.
companionPresenceMu sync.Mutex
companionSessions map[uint16]*gameSession
mu sync.Mutex
catalog *channelcatalog.Catalog
@@ -69,6 +69,21 @@ func currentSocketAvatarSocketTypes(catalog *pvfDungeonDropCatalog, itemID int64
return socketTypes, nil
}
// currentAvatarSocketTypesForOpen applies the avatar-part socket layout that
// the PVF's [avatar type select] does not encode explicitly. Rare coat/pants
// avatars carry one platinum (0x10) socket followed by two normal C sockets;
// their PVF section only lists the two C tags, so the opener used to produce a
// two-socket result. Keep other avatar parts and explicit three-socket layouts
// untouched.
func currentAvatarSocketTypesForOpen(rule currentEquipmentPlacementRule, socketTypes []byte) []byte {
out := append([]byte(nil), socketTypes...)
if (rule.targetSlot == 3 || rule.targetSlot == 4) &&
len(out) == 2 && out[0] == 0x04 && out[1] == 0x04 {
return []byte{0x10, 0x04, 0x04}
}
return out
}
func currentSocketEmblemType(catalog *pvfDungeonDropCatalog, itemID int64) byte {
if catalog == nil || itemID <= 0 || itemID > math.MaxUint32 {
return 0