Página 1 de 1

[Tutorial] Como ler um BIT de um BYTE

Enviado: 01 Dez 2021, 14:46
por tassioluis
Greetings from Vice City!
 
Enquanto eu trabalhava no meu projeto de criação de um mod para 2 jogadores do Vice City, me deparei com a necessidade de ler um único bit de um determinado byte.
 
Imagem
 
Para implementar um sistema de pulo, eu precisava saber se o personagem estava no ar (obviamente ele não poderia pular se estivesse). Deveria ser bastante simples fazer isso, mas o GTA3Script parece não ter sido preparado para esse tipo de operação, haja vista que o OPCODE de leitura de memória (MEMORY_READ) tem como argumento o BYTE e não o BIT, ou seja, ele só faz leitura de múltiplos de 8 bits (8, 16, 24, 32, etc...).

Mesmo assim, eu pensei que seria bem simples de recuperar aquele bendito bit pois, como sabemos, cada byte é composto de 8 bits, numa estrutura assim:
 
Imagem
Em outras palavras, o número daí de cima, em decimal, é 45, pois consiste na soma de cada parte dele (32+8+4+1).

Então agora ficou fácil! Já que o terceiro bit equivale a (devemos lembrar que a contagem começa em 0), basta que eu verifique se meu byte equivale a 8 (0000 1000), correto?

Não! (necessariamente)

Isso só seria verdade se o único bit alterado nesse byte fosse o terceiro, enquanto todos os outros permanecessem estáticos. No mundo real, normalmente isso não acontece. Mesmo que acontecesse, eu não poderia me dar ao luxo de confiar cegamente assim. No vídeo abaixo, no qual eu monitoro o valor do byte (em decimal) enquanto o mod roda, podemos ver que ele assume diversos valores:
 
 
Percebam que ele começa com 0 (0000 0000), depois vai para 32 (0010 0000), então 40 (0010 1000), 48 (0011 0000)... Ou seja, eu tenho diversos valores para o byte ao longo do tempo. Mas você pode pensar: "aaaahh, então basta eu pegar os valores cujo o terceiro bit seja 1 e verificar". Essa poderia ser uma saída, mas um pouco de análise combinatória, nos responderia que existem 128 valores possíveis para isso! (8 (0000 1000), 9 (0000 1001), 10 (0000 1010), 11 (0000 1011), 12 (0000 1100)...), o que torna impraticável aplicar isso em um código. A quantidade de IF seria muito grande! (neste caso 16 IF com 8 condições CADA)

Por outro lado, podemos perceber que o meu terceiro bit está funcionando como esperado pois ele só retorna 1 quando o personagem está de fato no ar. No vídeo, o byte assume o valor 40 (0010 1000), 42 (0010 1010)... Ou seja, a documentação tá correta e vai me ajudar a saber quando o personagem está no ar.

Mas voltando: então, como ler esse bit? Eu perguntei ao pessoal no grupo do discord, tanto na seção em português quanto em inglês e ambos me sugeriram utilizar opcodes somente presentes no CLEO do GTA San Andreas. Como estou codificando para o Vice City, obviamente não daria para fazer isso.

Vasculhando um pouco mais o GTA3Script, encontrei dois opcodes interessantes: BIT_SHLBIT_SHR
Mas afinal, o que seriam eles?

Um pouco de Google ajuda e logo de cara eu descobri que servem para deslocar os bits de um byte para a esquerda ou para a direita (agora o L e o R ali fazem sentido?)
 
Imagem
 
Se eu tiver um 6 (0000 0110) e aplicar um SHR de 2 (mover os bits 2 vezes para a direita), em teoria, ele se torna um 1 (0000 0001) (os números à direita, quando deslocados, se perdem se não houver "espaço").

OK, então agora bastaria que eu deslocasse meu querido byte em 4 para a esquerda, assim bastaria verificar se o resultado fosse maior que 127, afinal mesmo que todos os outros bytes, agora à direita do meu número, fossem 1, o valor máximo que eu conseguiria seria 127 (0111 1111), certo?

Não novamente! (caramba isso tá virando uma odisseia, né? mas modding não é isso? ficar testando as coisas...)

Acontece que (e eu não sei explicar por que devido ao meu conhecimento limitado terminei descobrindo) os valores anteriormente à esquerda vão mais para a esquerda! E os valores mostrados crescem, como por exemplo 640 (0010 1000 0000) (talvez estejam vazando para o byte anterior? Não é isso. Analisando um pouco melhor, me lembrei que o int (que é o que usamos para guardar o valor do byte) possui 4 bytes. Ou seja, teríamos que empurrar MUITO para a esquerda (mais precisamente, 60) para que os bits à esquerda desapareçam do nosso DWORD (4 bytes). A variável int é muito grande, se escrita em binário (0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000). Mas nada disso importa agora, visto que antes de perceber essa questão, encontrei uma outra maneira melhor e mais prática para ler o valor do bit)

Visto que isso não poderia me ajudar (até então), decidi buscar informações sobre os outros opcodes também relacionado a bits e que poderiam talvez possuir minha resposta. Um deles era o BIT_AND, que possuía 2 int de entrada e 1 de saída. Opa, parece que temos uma luz no fim do túnel... maaass como exatamente esse BIT_AND funciona?? 🤔

Voltamos pro Google e infelizmente "BIT" e "AND" são palavras extremamente comuns para se pesquisar, principalmente em se tratando de programação. Opcode database? Não tem nada que ajude de fato.

Então, como resolver? Testando!

Eu criei um código simples de debug (abaixo) para tentar entender o que esse OPCODE faz. A ideia era imputar diversos números e tentar interpretar os resultados.

SpoilerAbrir

Código: Selecionar tudo

SCRIPT_START

{
LVAR_INT a b res

a = 0
b = 0

main:
    IF IS_KEY_PRESSED VK_KEY_A
        a += 1
        WHILE IS_KEY_PRESSED VK_KEY_A
            WAIT 0
        ENDWHILE
    ENDIF

    IF IS_KEY_PRESSED VK_KEY_B
        b += 1
        WHILE IS_KEY_PRESSED VK_KEY_B
            WAIT 0
        ENDWHILE
    ENDIF

    BIT_AND a b res

    PRINT_FORMATTED_NOW "BIT_AND a: %i b: %i  =  %i" 1000 a b res

    WAIT 0

    GOTO main
}

SCRIPT_END
 
O par 1 e 2 me retornava 0.
2 e 2 retornava 2.
3 e 2 = 2.
5 e 2? 0.

hum, tá começando a ficar interessante...

Para quem ainda não percebeu, basicamente o que esse opcode está fazendo é operar AND em cada bit dos números. Ou seja, no caso de 1 (0001) e 2 (0010), o resultado é (0000). No caso de 3 (0011) e 2 (0010) o resultado é 2 (0010) (só o bit 1 faz o AND retornar 1). E assim para todo o resto...

O que aconteceria se eu operasse qualquer que fosse o int com o número 8 (0000 1000)? Exato! Ele só iria me retornar zeros ou oitos! Ou seja, acabei de encontrar uma maneira simples e prática de ler se o terceiro bit de um byte está ativo. Se ele estiver, vai me retornar um 8. Caso não, um zero.

Código: Selecionar tudo

[...]

GET_PED_POINTER scplayer2 memtemp
memtemp += 0x14D    //offset do CPed do byte que diz se está "nos céus" (airborne)
READ_MEMORY memtemp 1 0 var //optei por outra variável mas o próprio memtemp poderia receber o resultado

BIT_AND var 8 res     //verificando se o terceiro bit está ativo

IF res = 8
    PRINT_FORMATTED_NOW "O boneco ta voando!"
ELSE
    PRINT_FORMATTED_NOW "O boneco ta com os pes no chao!"
ENDIF

[...]
Isso vale para qualquer que seja o bit de um determinado byte. Se eu quisesse ler o 5º bit de um byte? Bastaria operar um BIT_AND da minha variável com 32 (0010 0000). E assim vai, acho que já deu pra entender, né?

E ainda podemos transformar isso em binário (caso vocês queiram, a meu ver, não parece ser tão útil), bastando que peguemos o nosso resultado e o desloque em 3 para a direita.

Código: Selecionar tudo

[...]

BIT_SHR var 3 res    //res agora possui valor 0 ou 1

[...]
Bem, é isso. A jornada de aprender a modificar as coisas é um pouco estressante e às vezes frustrante, mas demasiadamente gratificante.

Obrigado por me acompanhar até aqui e até o próximo tutorial! (se houver, né 🤷)

Re: [Tutorial] Como ler um BIT de um BYTE

Enviado: 04 Dez 2021, 10:08
por tassioluis
Analisando o IDB do Vice City, aparentemente a R* utilizou esses mesmos bits para saber se o personagem estava "controlável", creio que esse controlável no sentido de se poder fazer uma ação com ele, e não controlável pelo jogador.

A função CPed::IsPedInConrtol (end.: 0x00501950) retorna um bool (0 ou 1) pra saber se o personagem pode realizar alguma ação (correr para um lado, atirar, agachar, etc...), comparando se ele está fazendo alguma ação "bloqueável", se está no ar ou se recuperando de um pulo ou mesmo se está vivo.
 
Imagem

Eu não entendo muito de programação, mas deduzo que o argumento de entrada da função é um int e aquele "this" é o pointer do Ped em questão.

"DWORD", "BYTE" e "float" se referem ao tamanho dos campos.

580 em hexa vale 0x244, que é justamente o offset de "player current status" do Ped. Ela tá verificando se igual ou menor que 38 (0x26), pra saber se está fazendo ações que o Ped não está "bloqueado" (andando, atendendo o telefone, mirando, etc...)
 
Imagem

333 vale 0x14D, aquele byte de status que eu tava tentando tirar os bits. Tá lá os números 3 e 4, se referindo aos bits 3 e 4 daquele byte. Umas setinhas >> que provavelmente é o "Bit shift right" do C++ e um & (que em inglês é "and") seguido do numeral 1. Em termos práticos, a R* tá fazendo um BIT_AND com 8 e 16 para esses endereços. Aquela exclamação no começo da operação inverte o resultado, né? O que me deixa a pensar por que eles não fizeram um & com 0, o que retornaria o mesmo resultado prático.
 
Imagem

Já na última parte, tem um 852 (0x354), offset da saúde do Ped. Pelo visto, tá comparando com um outro float.
 
Imagem

Se formos até o float em questão, vemos que seu valor é 0.0! Ou seja, ela só quer saber se o personagem está vivo! (saúde maior que 0)

Engraçado é que o opcode GET_CHAR_HEALTH retorna um int de valor da saúde e não num float, o que faz até mais sentido, visto que a saúde no jogo sempre se apresenta como inteiro. Será que é armazenada como float e convertida e utilizada como int o tempo inteiro? Não sei, posso estar falando besteira ou ter entendido errado muitas coisas.

De qualquer forma, aparentemente eu tava usando uma lógica um pouco alinhada com a R* para saber se meu personagem poderia executar uma determinada ação (pular). Usei menos "comparadores" (no caso só o bit 3), mas tô bem feliz que é parecido com o que os programadores originais pensaram.

Eu vou estudar C++ pra passar a entender melhor o código pois tudo que falei acima foi pura dedução baseado no que conheço de GTA3Script dos tutoriais daqui da mixmods.

Mas o Junior_Djjr falou uma coisa bem interessante no post dele sobre Engenharia reversa:
Junior_Djjr escreveu:
27 Nov 2018, 16:29
Outra coisa interessante é que vocês podem usar funções C em seus mods CLEO etc. Basta procurar pelo nome da função, por exemplo strstr:

Código: Selecionar tudo

char * strstr (       char * str1, const char * str2 );

Código: Selecionar tudo

CALL_FUNCTION_RETURN 0x822650 2 2 ("RING" "STRING") (found)
IF found > 0
    //...
ENDIF
Lembrando que os argumentos são invertidos — eu ainda preciso muito fazer um tutorial de chamar funções em CLEO.

Para quem procura por endereços de funções (e offsets de structs, como já ensinei), um local ótimo é no source code do plugin-sdk...
Este é um assunto que vai muito longe... 

Será que dá pra usar a função original da R* no meu mod CLEO utilizando o endereço dela?

Fui! Tô indo lá testar!

Re: [Tutorial] Como ler um BIT de um BYTE

Enviado: 04 Dez 2021, 10:50
por tassioluis
Opa, funcionou sim!
 

Código: Selecionar tudo

SCRIPT_START

NOP

{

LVAR_INT memtemp scplayer pChar

main:
 
    GET_PLAYER_CHAR 0 scplayer
    GET_PED_POINTER scplayer pChar
    CALL_FUNCTION_RETURN 0x00501950 1 1 pChar memtemp   //bool CPed::IsPedInConrtol int
    BIT_AND memtemp 0x01 memtemp
    PRINT_FORMATTED_NOW "In control  =  %i" 1000 memtemp
    WAIT 0
    GOTO main
RETURN

}

SCRIPT_END
Imagem

Agora vamos explorar esse recurso!

Re: [Tutorial] Como ler um BIT de um BYTE

Enviado: 07 Dez 2021, 21:02
por Yoshin
Finalmente um tutorial sobre bitwise no gta3script, eu não conseguia usar esses opcodes chatos kk

Re: [Tutorial] Como ler um BIT de um BYTE

Enviado: 07 Dez 2021, 21:27
por tassioluis
Yoshin escreveu:
07 Dez 2021, 21:02
Finalmente um tutorial sobre bitwise no gta3script, eu não conseguia usar esses opcodes chatos kk

Huahshehahhshshehshshs

Tem muito opcode que não tá documentado. Isso atrapalha bastante. Tem outros que estão com "erro" em algumas definições. Por exemplo, tem um que diz que o argumento é um float do raio, quando na verdade é do diâmetro. Pode dar uma dor de cabeça tremenda.

Quando cê parte pra fazer engenharia reversa do código do jogo, aí que a coisa piora! A regra passa a ser "não estar bem documentado".

Mas de qualquer forma, aos trancos e barrancos, estou aprendendo muito. E isso é bastante gratificante.