Modificação no jogo com base em dll-wrapper'a

Há um jogo no Verbis Virtus com mecânica incomum - use feitiços usando um microfone.

Este não é um simulador de Hmayak Hakobyan, é um quebra-cabeça em primeira pessoa com controles atípicos.
Para isso, o jogo usa a biblioteca de reconhecimento de fala Sphinx.

A idéia parece interessante, mas a implementação foi tão positiva (muitas vezes o reconhecimento falha) e francamente irritante para ser lançada após os primeiros 20 minutos.
Sobre a aparência do lado de fora - geralmente silencioso.

Os desenvolvedores, infelizmente, não deixaram a capacidade de controlar feitiços do teclado, e eu decidi corrigi-lo.

O primeiro pensamento foi fazer alterações na biblioteca do Sphinx, por ser de código aberto. No entanto, descobri que há várias versões desta biblioteca.

Depois de experimentar três deles (correspondendo aproximadamente à época em que o jogo foi lançado), ainda não consegui encontrar o caminho certo, pois cada um tinha diferenças (pelo menos em termos do conjunto de funções exportadas).

Portanto, decidi criar um invólucro no topo da biblioteca original do jogo.

Para fazer isso, adotei a abordagem proposta no artigo Gerando Wrappers .DLL .

Sua essência é que você pode agrupar qualquer biblioteca sem o conhecimento dos parâmetros e tipos de funções exportadas, apenas seus nomes (que podem ser extraídos mesmo com um editor de texto) são suficientes.

A lista de exportação é criada usando o arquivo def do formulário:

EXPORTS func1=_func1 @1 func2=_func2 @2 

Os próprios wrappers de função são:

 _declspec(naked) void _func1() { __asm jmp dword ptr [procs + 1 * 4]; } 

Isso elimina os problemas de passar argumentos e retornar os valores das funções originais.

Primeiro, era necessário um pouco de engenharia reversa. Criei um wrapper com a única adição - registrando os nomes das funções chamadas.

Então, eu determinei onde, quando e como a lógica da biblioteca principal funciona.

Aconteceu que, a princípio, um certo número de amostras brutas do microfone foram coletadas pela função ps_process_raw () e, em seguida, a própria decisão foi tomada na função ps_get_hyp ().
Mais tarde (tarde demais), eu ainda pensei que valeria a pena examinar primeiro a documentação do Sphinx (onde tudo estava descrito).

Decidiu-se adicionar à função ps_process_raw () uma definição do estado das chaves que serão responsáveis ​​pelos feitiços.

Para fazer isso, você precisa atribuir essas chaves. Fazemos isso no DllMain (), além de obter os endereços das funções originais. Aqui estão alguns comerciais:

 BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { HINSTANCE hinst_dll; if (fdwReason == DLL_PROCESS_ATTACH) { hinst_dll = LoadLibraryA("pocketsphinx_orig.dll"); if (!hinst_dll) return 0; for (i = 0; i < 93; i++) procs[i] = GetProcAddress(hinst_dll, import_names[i]); for (i = 0; i < 256; i++) { _itoa(i, &buf[i][0], 10); GetPrivateProfileStringA("main", &buf[i][0], 0, &buf[i][0], MAX_PATH, ".\\settings.ini"); } i = 0; } else if (fdwReason == DLL_PROCESS_DETACH) FreeLibrary(hinst_dll); return 1; } 

O arquivo settings.ini possui o formato:

 [main] 49=String 1 50=String 2 

Total, na matriz buf, haverá linhas correspondentes aos feitiços. Além disso, eles se situam em índices correspondentes às chaves necessárias.

Determinaremos o estado das chaves da seguinte maneira:

 void find_key() { if(!i) { for (i = 0; i < 256; i++) if (buf[i][0]) if (GetAsyncKeyState(i) >> 1) { i = (int)&buf[i][0]; return; } if (i == 256) i = 0; } } 

O wrapper da função ps_process_raw () terá a seguinte aparência:

  _declspec(naked) void _ps_process_raw() { find_key(); __asm jmp dword ptr [procs + 78 * 4]; } 

Ou seja, se, no momento em que for necessário transmitir ao microfone, o usuário pressionou uma tecla, o ponteiro para a linha correspondente à tecla pressionada foi salvo na variável global i.

Os preparativos estão terminados, é hora de implementar a funcionalidade básica.

É necessário determinar se o botão ortográfico foi pressionado pelo usuário e, em caso afirmativo, alterar o valor de retorno na função ps_get_hyp ().

Isso exigirá um pouco de manipulação da pilha:

  _declspec(naked) void _ps_get_hyp() { static unsigned int return_address; _asm { //save return address push eax mov eax, dword ptr [esp+4] mov return_address, eax pop eax //call original ps_get_hyp add esp, 4 call dword ptr [procs + 22 * 4] sub esp, 4 //replace result (if key was pressed) cmp i, 0 je end mov eax, i xor ecx,ecx mov i, ecx end: //restore return address push eax mov eax, return_address mov dword ptr [esp+4], eax pop eax ret } } 

A principal funcionalidade está em uma peça com o comentário "substituir resultado (se a tecla foi pressionada)".
Se o ponteiro estiver na variável global, substituímos o resultado retornado e redefinimos a variável global.

E se não, então deixamos tudo inalterado.

Assim, você pode continuar transmitindo pelo microfone ou pode usar os botões (eles têm prioridade). O objetivo é alcançado.

Sim, existem pontos tortos na solução.

Por exemplo, passando um ponteiro por uma variável global, também chamada i (decidi usá-lo novamente após a inicialização no DllMain).

Escalar a pilha de outra pessoa também não é aceito de alguma forma (não pensei em como fazê-lo de maneira diferente).

No entanto, a solução está funcionando. O código principal tem menos de 100 linhas; na maioria das vezes, tudo é trivial.

Código fonte
arquivo def
Arquivo de configurações binárias +

Instalação:

  • Na pasta \ In Verbis Virtus \ Binaries \ Win32 \, renomeie o pocketsphinx.dll original para pocketsphinx_orig.dll
  • Coloque o wrapper pocketsphinx.dll nas proximidades
  • Na pasta \ In Verbis Virtus \ Binaries \ Win32 \ UserCode, coloque settings.ini

Críticas e sugestões são aceitas.

Source: https://habr.com/ru/post/pt437884/


All Articles