Improve mod library and example mod.

This commit is contained in:
lzwdgc 2024-02-12 21:40:20 +03:00
parent 091dd3cbe9
commit 3d76460d56
7 changed files with 2833 additions and 144 deletions

View file

@ -0,0 +1,381 @@
#include <string>
#include <string_view>
#include <stdint.h>
#include <windows.h>
/*struct char20 {
char string[20];
};*/
#define _BYTE uint8_t
#define this
#define __unaligned
#include "aim.exe.h"
#undef this
#undef __unaligned
using namespace std::literals;
constexpr auto call_command_length = 5;
enum aim1_fix : uint32_t {
script_function__ISGLIDER = 0x0043A1F6,
trade_actions_weapon_checks = 0x004072FA,
setup_proper_weapon_slots_for_a_glider = 0x004D62E4,
put_weapon_into_the_right_slot_after_purchase = 0x00417A6D,
sell_correct_weapon = 0x004176BC,
empty_light_weapon_message = 0x004067C4,
empty_heavy_weapon_message = 0x0040688B,
};
uint32_t known_caller;
auto player_ptr = (Player **)0x005B7B38;
auto get_player_ptr() {
auto player_ptr2 = (Player *)0x00678FEC;
return player_ptr2;
}
enum class glider_id : uint32_t {
nargoon = 0x7,
eyedstone = 0xe,
finder1 = 0x19,
finder2 = 0x1A,
};
#define NOP1 __asm { nop }
#define NOP2 NOP1 NOP1
#define NOP4 NOP2 NOP2
#define NOP8 NOP4 NOP4
#define NOP16 NOP8 NOP8
#define NOP32 NOP16 NOP16
#define NOP64 NOP32 NOP32
#define NOP128 NOP64 NOP64
#define NOP256 NOP128 NOP128
#define NOP512 NOP256 NOP256
#define NOP1024 NOP512 NOP512
#define NOP4K NOP1024 NOP1024 NOP1024 NOP1024
bool __stdcall strequ(const char *s1, const char *s2) {
do {
if (*s1 != *s2) {
return false;
}
} while (*s1++ && *s2++);
return true;
}
__declspec(naked) void fix_script_function__ISGLIDER() {
__asm {
; restore values
popad
pushad
mov edi, eax ; my glider
mov esi, [esp+20h+4+8*4] ; arg
label_cmp:
mov al, [edi]
mov cl, [esi]
cmp al, cl
jne bad
cmp al, 0
je end
inc edi
inc esi
jmp label_cmp
end:
cmp al, cl
jne bad
popad
; push some regs as required by success branch
push ebx
push ebp
push esi
push edi
push 0x0043A2DA
ret
bad:
popad
; original thunk
copy:
mov cl, [eax]; injection point
inc eax
mov[edx], cl
inc edx
test cl, cl
jnz copy
; epilogue
push known_caller
ret
}
}
bool is_double_light_weapons_glider() {
return false
|| get_player_ptr()->glider_name.idx == (int)glider_id::nargoon
|| get_player_ptr()->glider_name.idx == (int)glider_id::finder1
;
}
bool is_double_heavy_weapons_glider() {
return false
|| get_player_ptr()->glider_name.idx == (int)glider_id::eyedstone
|| get_player_ptr()->glider_name.idx == (int)glider_id::finder2
;
}
bool is_special_glider() {
return is_double_light_weapons_glider() || is_double_heavy_weapons_glider();
}
bool is_double_light_weapons_glider(glider_id id) {
return false
|| id == glider_id::nargoon
|| id == glider_id::finder1
;
}
bool is_double_heavy_weapons_glider(glider_id id) {
return false
|| id == glider_id::eyedstone
|| id == glider_id::finder2
;
}
bool is_special_glider(glider_id id) {
return is_double_light_weapons_glider(id) || is_double_heavy_weapons_glider(id);
}
bool __stdcall can_buy_weapon(int selected_weapon_type, int *canbuy) {
if (is_special_glider()) {
if (selected_weapon_type == light && is_double_heavy_weapons_glider()) {
*(uint8_t*)canbuy = 0;
}
if (selected_weapon_type == heavy && is_double_light_weapons_glider()) {
*(uint8_t *)canbuy = 0;
}
return true;
}
return false;
}
__declspec(naked) void fix_trade_actions_weapon_checks() {
__asm {
; restore values
popad
pushad
; ebx = selected weapon type in the store
; esi = gun_desc
; [esp + 102h] bool is_buy_change
; [esp + 103h] bool can_buy
mov edx, esp
add edx, 123h
lea edx, [edx]
push edx
push ebx
call can_buy_weapon
test al, 1
jz epi
popad
push 0x00407308
ret
epi:
; epilogue
popad
mov eax, ebx
sub eax, 0
push known_caller
ret
}
}
bool can_have_light_weapon_slot(glider *obj, glider_id id, int special) {
if (is_special_glider(id)) {
return true;
}
return ((special >> 1) & 0x1) == 0;
}
bool can_have_heavy_weapon_slot(glider *obj, glider_id id, int special) {
if (is_special_glider(id)) {
return true;
}
return obj->standard > 2 && (special & 0x1) == 0;
}
void __stdcall can_have_weapon_slots(glider *gliders, glider *obj, int special) {
auto id = obj - gliders;
obj->can_have_light_weap = can_have_light_weapon_slot(obj, (glider_id)id, special);
obj->can_have_heavy_weap = can_have_heavy_weapon_slot(obj, (glider_id)id, special);
}
__declspec(naked) void fix_can_have_weapon_slots_for_a_glider() {
__asm {
; restore values
popad
pushad
push eax; SPECIAL field value
mov ecx, edx;
add ecx, ebx;
push ecx ; glider*
push edx ; glider**
call can_have_weapon_slots
popad
push 0x004D6351
ret
}
}
bool __stdcall put_purchased_weapon_into_light_slot(int purchased_weapon_type) {
if (is_special_glider()) {
return get_player_ptr()->glider_object->light_gun_id == -1;
}
return purchased_weapon_type == 0;
}
__declspec(naked) void fix_put_weapon_into_the_right_slot_after_purchase() {
__asm {
; restore values
popad
pushad
push eax ; purchased weapon type
call put_purchased_weapon_into_light_slot
cmp al, 1
jne heavy_exit
popad
push 0x00417AA1
ret
heavy_exit:
popad
push 0x00417A75
ret
}
}
bool __stdcall sell_light_weapon(int sold_weapon_type) {
if (is_special_glider()) {
return get_player_ptr()->glider_object->light_gun_id != -1;
}
return sold_weapon_type == 0;
}
__declspec(naked) void fix_sell_correct_weapon() {
__asm {
; restore values
popad
pushad
push eax; purchased weapon type
call sell_light_weapon
cmp al, 1
jne heavy_exit
popad
push 0x004176F1
ret
heavy_exit:
popad
push 0x004176C4
ret
}
}
uint32_t empty_weapon_message;
void __stdcall fix_empty_weapon_message(int light_slot) {
constexpr auto INT_EMPTY_LIGHT_GUN = 0x00521AA8;
constexpr auto INT_EMPTY_HEAVY_GUN = 0x00521A94;
if (is_double_light_weapons_glider()) {
empty_weapon_message = INT_EMPTY_LIGHT_GUN;
} else if (is_double_heavy_weapons_glider()) {
empty_weapon_message = INT_EMPTY_HEAVY_GUN;
} else {
empty_weapon_message = light_slot ? INT_EMPTY_LIGHT_GUN : INT_EMPTY_HEAVY_GUN;
}
}
__declspec(naked) void fix_empty_light_weapon_message() {
__asm {
; restore values
popad
pushad
mov eax, 1
push eax
call fix_empty_weapon_message
popad
push empty_weapon_message
push known_caller
ret
}
}
__declspec(naked) void fix_empty_heavy_weapon_message() {
__asm {
; restore values
popad
pushad
mov eax, 0
push eax
call fix_empty_weapon_message
popad
push empty_weapon_message
push known_caller
ret
}
}
extern "C" __declspec(dllexport) __declspec(naked) void dispatcher() {
__asm {
pop known_caller
pushad
}
switch (known_caller - call_command_length) {
case aim1_fix::script_function__ISGLIDER:
known_caller += 0xA - call_command_length; // make normal ret
__asm jmp fix_script_function__ISGLIDER
break;
case aim1_fix::trade_actions_weapon_checks:
__asm jmp fix_trade_actions_weapon_checks
break;
case aim1_fix::setup_proper_weapon_slots_for_a_glider:
__asm jmp fix_can_have_weapon_slots_for_a_glider
break;
case aim1_fix::put_weapon_into_the_right_slot_after_purchase:
__asm jmp fix_put_weapon_into_the_right_slot_after_purchase
break;
case aim1_fix::sell_correct_weapon:
__asm jmp fix_sell_correct_weapon
break;
case aim1_fix::empty_light_weapon_message:
__asm jmp fix_empty_light_weapon_message
break;
case aim1_fix::empty_heavy_weapon_message:
__asm jmp fix_empty_heavy_weapon_message
break;
default:
break;
}
// just return
__asm {
popad
push known_caller
ret
}
}
void setup() {
//constexpr uint32_t free_data_base = 0x006929C0;
constexpr uint32_t free_data_base = 0x00692FF0;
auto mem = (uint8_t *)free_data_base;
mem[0] = 0xE9;
*(uint32_t*)&mem[1] = (uint32_t)&dispatcher - free_data_base - call_command_length;
}
BOOL WINAPI DllMain(HINSTANCE, DWORD fdwReason, LPVOID) {
if (fdwReason == DLL_PROCESS_ATTACH) {
setup();
}
return TRUE;
}

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,9 @@ deps: pub.lzwdgc.Polygon4.Tools.aim1_mod_maker-master
* You can also distribute mod archive (requires 7z program).
**/
#define AIM_TYPES_FILE_NAME "aim.exe.h"
#define INJECTIONS_FILE_NAME "aim.exe.fixes.h"
#ifndef INJECTED_DLL
#include "aim1_mod_maker.h"
@ -19,7 +22,7 @@ deps: pub.lzwdgc.Polygon4.Tools.aim1_mod_maker-master
// patch note: lz, Solant, Streef
// patch note:
// patch note: Description
// patch note: This mod fixes some issues with the original AIM v1.04 game.
// patch note: This mod fixes some issues with the original AIM v1.06 (DRM-free) (1.0.6.3) game.
// patch note:
// patch note: Installation
// patch note: Unpack and drop all files near original aim.exe. Replace files if necessary.
@ -27,6 +30,16 @@ deps: pub.lzwdgc.Polygon4.Tools.aim1_mod_maker-master
int main(int argc, char *argv[]) {
mod_maker mod;
mod.files_to_distribute.insert(INJECTIONS_FILE_NAME);
mod.files_to_distribute.insert(AIM_TYPES_FILE_NAME);
// patch note: enable double weap gliders (still have many bugs related)
mod.make_injection(0x004072FA); // can trade for buy purposes
mod.make_injection(0x004D62E4); // setup proper weapon slots for a glider
mod.make_injection(0x00417A6D); // put weapon into the right slot after purchase
mod.make_injection(0x004176BC); // sell correct weapon
mod.make_injection(0x004067C4); // empty light weap
mod.make_injection(0x0040688B); // empty heavy weap
// patch note: CHANGES
// patch note:
@ -65,7 +78,6 @@ int main(int argc, char *argv[]) {
// patch note:
// patch note: Script Changes
// patch note: _ISGLIDER() function can check exact glider name now, for example _ISGLIDER(GL_M3_A_FIRST1) (lz)
// patch note: fix joining First Ones having one of four required gliders (lz, Streef)
mod.replace("ORG_FIRST.scr",
"IF(_PLAYERHAS(GL_M3_A_FIRST1)||_PLAYERHAS(GL_M3_A_FIRST1))",
@ -77,24 +89,30 @@ int main(int argc, char *argv[]) {
mod.replace("ORG_SINIGR.scr",
"IF(_PLAYERHAS(GL_S2_PA_SINYGR)|_PLAYERHAS(GL_S4_S_SINYGR))",
"IF(_ISGLIDER(GL_S2_PA_SINYGR)|_ISGLIDER(GL_S4_S_SINYGR))");
// patch note: _ISGLIDER() function can check exact glider name now, for example _ISGLIDER(GL_M3_A_FIRST1) (lz)
mod.make_injection(0x0043A1F6, 10);
//
// end of scripts section
// patch note:
/*mod.replace("Script/bin/B_L1_BASE1.scr", "_ADDBALANCE(300)", R"(
mod.replace("Script/bin/B_L1_BASE1.scr", "_ADDBALANCE(300)", R"(
_ADDBALANCE(300 )
_ADDOBJECT(GL_M3_PA_EYEDSTONE)
_ADDOBJECT(GL_S3_PS_FINDER1)
_ADDOBJECT(EQP_VACUUM_DRIVE_S3)
_ADDOBJECT(EQP_ZERO_ARMOR_S3)
_ADDOBJECT(EQP_SHIELD_GENERATOR4_S3)
_ADDOBJECT(GL_M4_S_FIRST2)
_ADDOBJECT(EQP_VACUUM_DRIVE_S4)
//_ADDOBJECT(GL_M4_S_FIRST2)
//_ADDOBJECT(EQP_VACUUM_DRIVE_S4)
//_ADDOBJECT(EQP_MEZON_REACTOR_S4)
//_ADDOBJECT(EQP_GLUON_REACTOR_S1)
_ADDOBJECT(EQP_ZERO_ARMOR_S4)
_ADDOBJECT(EQP_SHIELD_GENERATOR4_S4)
_ADDOBJECT(GUN_MICROWAVE_OSCILLATOR)
_ADDOBJECT(GUN_RAILGUN)
//_ADDOBJECT(EQP_ZERO_ARMOR_S4)
//_ADDOBJECT(EQP_SHIELD_GENERATOR4_S4)
//_ADDOBJECT(GUN_MICROWAVE_OSCILLATOR)
//_ADDOBJECT(GUN_RAILGUN)
_ADDRATING(300000000)
_ADDBALANCE(30000000)
@ -115,7 +133,7 @@ int main(int argc, char *argv[]) {
_SETEVENT(SECTOR7.ACCESS)
//_SETEVENT(SECTOR8.VISIT)
_SETEVENT(SECTOR8.ACCESS)
)");*/
)");
// patch note: Release Manager
// patch note: lz
@ -144,100 +162,5 @@ int main(int argc, char *argv[]) {
}
#else // INJECTED_DLL
#include <stdint.h>
#include <windows.h>
constexpr auto call_command_length = 5;
enum aim1_fix : uint32_t {
script_function__ISGLIDER = 0x0043A1F6,
};
uint32_t known_caller;
__declspec(naked) void fix_script_function__ISGLIDER() {
__asm {
; restore values
popad
pushad
mov edi, eax; my glider
mov esi, [esp + 20h + 4 + 8 * 4]; arg
label_cmp :
mov al, [edi]
mov cl, [esi]
cmp al, cl
jne bad
cmp al, 0
je end
cmp cl, 0
je end
inc edi
inc esi
jmp label_cmp
end :
cmp al, cl
jne bad
popad
; push some regs as required by success branch
push ebx
push ebp
push esi
push edi
push 0x0043A2DA
ret
bad :
popad
; popf
; original thunk
copy :
mov cl, [eax]; injection point
inc eax
mov[edx], cl
inc edx
test cl, cl
jnz copy
; epilogue
push known_caller
ret
}
}
extern "C" __declspec(dllexport) __declspec(naked) void dispatcher() {
__asm {
pop known_caller
pushad
}
switch (known_caller - call_command_length) {
case aim1_fix::script_function__ISGLIDER:
known_caller += 0xA - call_command_length; // make normal ret
__asm jmp fix_script_function__ISGLIDER
break;
default:
break;
}
// just return
__asm {
popa
push known_caller
ret
}
}
void setup() {
constexpr uint32_t free_data_base = 0x006929C0;
auto mem = (uint8_t *)free_data_base;
mem[0] = 0xE9;
*(uint32_t *)&mem[1] = (uint32_t)&dispatcher - free_data_base - call_command_length;
}
BOOL WINAPI DllMain(HINSTANCE, DWORD fdwReason, LPVOID) {
if (fdwReason == DLL_PROCESS_ATTACH) {
setup();
}
return TRUE;
}
#include INJECTIONS_FILE_NAME
#endif

View file

@ -51,6 +51,18 @@ auto operator""_bin(const char *ptr, uint64_t len) {
return ret;
}
struct aim_exe_v1_06_constants {
enum : uint32_t {
trampoline_base_real = 0x00025100,
trampoline_target_real = 0x001207f0,
//code_base = 0x00401000,
//data_base = 0x00540000,
//free_data_base_virtual = 0x006929C0,
free_data_base_virtual = 0x00692FF0,
our_code_start_virtual = 0x005207F0, // place to put out dll load
};
};
struct mod_maker {
enum class file_type {
unknown,
@ -164,6 +176,24 @@ struct mod_maker {
return patch_raw(fn, offset, oldval, val);
}
// all you need is to provide injection address (virtual) with size
// handle the call instruction in 'dispatcher' symbol (naked) of your dll
constexpr static inline auto call_command_length = 5;
void make_injection(uint32_t virtual_address, uint32_t size = call_command_length) {
uint32_t len = size;
if (len < call_command_length) {
throw std::runtime_error{"jumppad must be 5 bytes atleast"};
}
primitives::templates2::mmap_file<uint8_t> f{find_real_filename(aim_exe),
primitives::templates2::mmap_file<uint8_t>::rw{}};
auto ptr = f.p + virtual_address - aim_exe_v1_06_constants::our_code_start_virtual + aim_exe_v1_06_constants::trampoline_target_real;
memcpy(ptr, make_insn_with_address("e8"_bin, aim_exe_v1_06_constants::free_data_base_virtual -
(virtual_address + call_command_length)));
memcpy(ptr, make_nops(len - call_command_length));
std::println("making injection on the virtual address 0x{:0X} (real address 0x{:0X}), size {}", virtual_address, ptr - f.p,
size);
}
#define ENABLE_DISABLE_FUNC(name, enable, disable) \
void enable_##name() { name(enable); } \
void disable_##name() { name(disable); }
@ -226,7 +256,7 @@ private:
*(uint32_t *)(&arr[insn.size()]) = addr;
return arr;
}
static auto make_nops(uint32_t len) {
static byte_array make_nops(uint32_t len) {
byte_array arr(len, 0x90);
return arr;
}
@ -279,28 +309,26 @@ private:
make_injected_dll();
files_to_distribute.insert(aim_exe);
primitives::templates2::mmap_file<uint8_t> f{find_real_filename(aim_exe), primitives::templates2::mmap_file<uint8_t>::rw{}};
constexpr uint32_t trampoline_base = 0x00025100;
constexpr uint32_t trampoline_target = 0x001207f0;
constexpr uint32_t code_base = 0x00401000;
constexpr uint32_t data_base = 0x00540000;
constexpr uint32_t free_data_base = 0x006929C0;
//constexpr uint32_t our_data = 0x00550FD0;
const uint32_t our_data_start = 0x005207F0;
uint32_t our_data = 0x005207F0;
//constexpr uint32_t free_data_base_real = 0x140000 + our_data - 0x00540000;
uint32_t our_data = aim_exe_v1_06_constants::our_code_start_virtual;
auto ptr = f.p + trampoline_target;
//strcpy((char *)f.p + free_data_base_real, get_dll_name().c_str());
strcpy((char *)ptr, get_dll_name().c_str());
auto ptr = f.p + aim_exe_v1_06_constants::trampoline_target_real;
#ifdef NDEBUG
auto dllnamelen = get_sw_dll_name().size() + 1;
strcpy((char *)ptr, get_sw_dll_name().c_str());
#else
auto dllname = "h:\\Games\\AIM\\1\\.sw\\out\\d\\aim_fixes-0.0.1.dll"s;
auto dllnamelen = dllname.size() + 1;
strcpy((char *)ptr, dllname.c_str());
#endif
ptr += dllnamelen;
auto push_dll_name = make_insn_with_address("68"_bin, our_data); // push
ptr += 0x20;
our_data += 0x20;
strcpy((char *)ptr, "dispatcher");
auto dispatcher_func_name = make_insn_with_address("68"_bin, our_data); // push
ptr += 0x20;
our_data += 0x20;
const auto jumppad = "68 30 B8 51 00"_bin; // push offset SEH_425100
uint32_t jump_offset = ptr - f.p - trampoline_base - jumppad.size() * 2;
uint32_t jump_offset = ptr - f.p - aim_exe_v1_06_constants::trampoline_base_real - jumppad.size() * 2;
memreplace(f.p, f.sz, jumppad, make_insn_with_address("e9"_bin, jump_offset));
memcpy(ptr, jumppad); // put our removed insn
memcpy(ptr, R"(
@ -331,15 +359,7 @@ FF D7 ; call edi
memcpy(ptr, R"(
61 ; popa
)"_bin);
memcpy(ptr, make_insn_with_address("e9"_bin, -(ptr - f.p - trampoline_base - jumppad.size())));
// E8 C5 87 25 00
constexpr auto call_command_length = 5;
uint32_t start_addr = 0x0043A1F6;
uint32_t len = 10;
ptr = f.p + start_addr - our_data_start + trampoline_target;
memcpy(ptr, make_insn_with_address("e8"_bin, free_data_base - (start_addr + call_command_length)));
memcpy(ptr, make_nops(len - call_command_length));
memcpy(ptr, make_insn_with_address("e9"_bin, -(ptr - f.p - aim_exe_v1_06_constants::trampoline_base_real - jumppad.size())));
}
path find_real_filename(path fn) const {
auto s = fn.wstring();

View file

@ -22,4 +22,43 @@
</DisplayString>
</Type>
<Type Name="db2::tab">
<Expand>
<Item Name="[number of tables]" ExcludeView="simple">n_tables</Item>
<Item Name="[number of fields]" ExcludeView="simple">n_fields</Item>
<ArrayItems>
<Size>n_tables</Size>
<ValuePointer>(db2::tab::table*)(&amp;n_fields+1)</ValuePointer>
</ArrayItems>
<ArrayItems>
<Size>n_fields</Size>
<ValuePointer>(db2::tab::field*)((db2::tab::table*)(&amp;n_fields+1)+n_tables)</ValuePointer>
</ArrayItems>
</Expand>
</Type>
<Type Name="db2::tab::table">
<DisplayString>
{id},{name,s}
</DisplayString>
</Type>
<Type Name="db2::tab::field">
<DisplayString>
{table_id},{id},{name,s},{type}
</DisplayString>
</Type>
<Type Name="db2::ind">
<Expand>
<Item Name="[number of values]" ExcludeView="simple">n_values</Item>
<ArrayItems>
<Size>n_values</Size>
<ValuePointer>(db2::value*)(&amp;n_values+1)</ValuePointer>
</ArrayItems>
</Expand>
</Type>
<Type Name="db2::value">
<DisplayString>
{table_id},{name,s}
</DisplayString>
</Type>
</AutoVisualizer>

View file

@ -183,7 +183,7 @@ struct Good
};
std::string name;
char unk1[0x40];
char condition_variable[0x40]; // when this var is set, we can access the good
float unk1_2 = 0;
float price = 0; // unk, quantity?
float unk2[10];
@ -199,7 +199,7 @@ struct Good
{
READ_STRING(b, name);
if (gameType == GameType::Aim1)
READ(b, unk1);
READ(b, condition_variable);
else
READ(b, unk1_2);
READ(b, price);

17
sw.cpp
View file

@ -8,10 +8,13 @@ void build(Solution &s)
auto cppstd = cpp23;
auto &common = tools.addStaticLibrary("common");
{
common += cppstd;
common.setRootDirectory("src/common");
common += ".*"_rr;
common.Public += "pub.egorpugin.primitives.filesystem"_dep;
common.Public += "pub.egorpugin.primitives.templates2"_dep;
}
auto add_exe_base = [&](const String &name) -> decltype(auto)
{
@ -73,16 +76,16 @@ void build(Solution &s)
;
}
auto &aim1_mod_maker = tools.addStaticLibrary("aim1_mod_maker");
auto &aim1_mod_maker = add_exe_with_common("aim1_mod_maker"); // actually a library
aim1_mod_maker.Public += "pub.egorpugin.primitives.command"_dep;
auto &aim1_community_fix = tools.addExecutable("examples.mods.aim1_community_fix");
{
auto &t = aim1_mod_maker;
auto name = "aim1_mod_maker";
auto &t = aim1_community_fix;
t.PackageDefinitions = true;
t += cppstd;
t.setRootDirectory("src/"s + name);
//t.Public += "pub.egorpugin.primitives.sw.main"_dep;
t.Public += "pub.egorpugin.primitives.command"_dep;
t.Public += common;
t += "examples/mods/aim1_community_fix/.*"_rr;
t += aim1_mod_maker;
}
add_exe("mod_reader") += model;