M.2. Script/Thread Memory
Enviado: 17 Ago 2018, 16:56
Já aprendeu sobre manipulação de memória? Já brincou um pouco? Que tal agora nos aprofundarmos um pouco mais?Concluindo esta parte você irá:
Aprender a usar e saber dizer as utilidades do Thread Memory, mesmo que não aprenderá completamente todas as utilidades.
Do mesmo modo que você pode ler/escrever memórias do jogo, você pode escrever memórias do seu próprio script... ou de tudo mais o que você imaginar.
O que é "Thread Memory"?
Primeiramente, o nome não é tão correto. Leia o que é este "thread" no glossário do início do tutorial. Seria mais correto chamar de "Script Memory", mas o nome "Thread" pegou.
Tal coisa só é útil devido à SCM/CLEO ter limite de variáveis, além de umas limitações no uso de escopos que impossibilita criar variáveis válidas por todo o código.
Thread Memory nada mais é do que um espaço extra para armazenar dados dentro do seu próprio script, assim resolvendo tais limitações. De fato, este é um tópico que você percebe o quão SCM/CLEO é fraco comparado ao MoonLoader.
Quando a CLEO ser atualizada com variáveis ilimitadas (fazemos a mínima ideia de quando isso acontecerá), este tópico ficará quase inútil.
Se você curte um lado mais "low level" de programação, gostará disto, principalmente se você deseja desafio e é masoquista.
Como criar "Thread Memory"
É super comum em códigos mais avançados serem criados os chamados por nós de "thread memory".
Thread memory é um espaço de dados que você reserva dentro do seu próprio script:
Código: Selecionar tudo
Memory:
DUMP
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
ENDDUMPO exemplo acima eu criei um thread memory de 4 x 4 bytes, ou seja, total de 16 bytes. Basta contar a quantidade de
00, onde cada 00 é 1 byte.Organizei eles com 4 linhas de 4 bytes, não por questão de funcionamento, e sim por legibilidade / organização.
Ou seja, eu poderia ter organizado estes 16 bytes de qualquer maneira que ainda assim iria funcionar, por exemplo tudo em uma só linha.
O que eu fiz aqui foi inserir um código hex dentro do meu próprio script. Mas por enquanto pense nisso simplesmente como "um espaço de memória onde podemos armazenar informações".
Veja um exemplo onde eu simplesmente escrevi o valor "100" nos primeiros 4 bytes daquela minha thread memory:
Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT pThreadMemory
GET_LABEL_POINTER Memory (pThreadMemory)
WRITE_MEMORY pThreadMemory 4 (100) FALSE
}
SCRIPT_END
Memory:
DUMP
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
ENDDUMPCódigo: Selecionar tudo
GET_LABEL_POINTER Memory (pThreadMemory)
READ_MEMORY pThreadMemory 4 FALSE (iValue)E então? Eu tenho 4 linhas de 4 bytes, eu só usei uma. Como faço para usar as próximas linhas? Pense.
Olhe para o código, tente imaginar. Você deve saber.
...
Sim, simplesmente aumentando 4 bytes no endereço da label para ir próxima linha, e mais 4 para ir pra próxima.
Assim podemos escrever diferentes números lá em diferentes linhas:
Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT pThreadMemory
GET_LABEL_POINTER Memory (pThreadMemory)
WRITE_MEMORY pThreadMemory 4 (100) FALSE
pThreadMemory += 4
GET_LABEL_POINTER Memory (pThreadMemory)
WRITE_MEMORY pThreadMemory 4 (53022) FALSE
pThreadMemory += 8
GET_LABEL_POINTER Memory (pThreadMemory)
WRITE_MEMORY pThreadMemory 4 (500.0) FALSE
}
SCRIPT_END
Memory:
DUMP
00 00 00 00 // <- 100
00 00 00 00 // <- 53022
00 00 00 00
00 00 00 00 // <- 500.0
ENDDUMPComo evitar Thread Memory: Allocate Memory
Thread memory é ótimo para casos menores, mas para maiores quantidades de bytes, principalmente se você precisa de uma quantidade variável de bytes, prefira usar
ALLOCATE_MEMORY, que irá criar um espaço de determinada quantidade de bytes para você depois poder limpar com FREE_MEMORY (quando o script ser encerrado, jogo reiniciado etc também limpa).ALLOCATE_MEMORY é bom mas causa fragmentação da memória RAM, portanto evite usar demais em pedaços pequenos a não ser que realmente necessário.Utilidades
Certo, mas qual a real utilidade disso tudo?
Agora vamos focar mais nas utilidades e aprender mais na prática. Depois voltamos com mais explicações.
Manipulação de strings com mais de 16 bytes.
Um dos usos mais comuns é o uso de strings longas demais para caber em variáveis, assim, você pode improvisar:
Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT pThreadMemory
GET_LABEL_POINTER Memory (pThreadMemory)
STRING_FORMAT (pThreadMemory) "O pato anda de costas quando o carro capota".
PRINT_STRING_NOW $pThreadMemory 5000
}
SCRIPT_END
Memory:
DUMP
// 64 bytes
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ENDDUMPO que eu fiz foi pegar o endereço da thread memory, usar o comando
STRING_FORMAT para escrever o valor do texto lá, e, pronto. Agora eu posso usar a variável com o endereço como se fosse uma string. O que é bem interessante, não?Na real, com thread memory você pode ter strings de tamanhos ilimitados! — mas se o jogo / cleo aceita qualquer tamanho é outro assunto :)Perceba que noSTRING_FORMATvocê tem que colocar a variável de retorno no início e não no fim como você está acostumado, é porque é um comando da CLEO 4 e somos muito desorganizados!
Perceba também que não é sempre que você poderá usar strings desta forma. Há comandos que aceitam, e outros não.
Por exemplo, para ler e escrever arquivos .ini usando o endereço do arquivo como variável, só é possível usar isto a partir da versão 4.3.23 (onde o Fabio que deixou atualizou à pedido meu).
Por último, saiba que oSTRING_FORMATadiciona um null terminator (00) no fim da string normalmente, você não precisa ter um00sobrando para isso. Por outro lado, cuidado! Se você tem uma thread memory de 16 bytes, se você escrever uma string de 16 caracteres lá, o null terminator será o 17º, assim sobrescrevendo o próximo byte do seu script! Sempre tenha bytes sobrando para evitar isso.
Lembre-se que entradas GXT só suportam 128 bytes (127 caracteres, sendo o último byte um null terminator).
Abaixo, um exemplo disto para criar uma entrada GXT, e mostrá-la na tela por um comando que só aceita entrada GXT (
DISPLAY_TEXT):Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT scplayer
LVAR_INT pThreadMemory
LVAR_FLOAT x y z
GET_PLAYER_CHAR 0 scplayer
main_loop:
WAIT 0
// Peguei a coordenada do player
GET_CHAR_COORDINATES scplayer (x y z)
// Peguei o ponteiro pra thread memory
GET_LABEL_POINTER Memory pThreadMemory
// Formatei uma string com os valores das coordenadas.
// Usei "%f" para indicar que quero adicionar um float ali.
// mais especificamente, "%.3f" para dizer que eu quero 3 números de precisão pra direita.
STRING_FORMAT (pThreadMemory) "%.3f %.3f %.3f" x y z
// Apliquei a string que criei. Agora quando eu usar a entrada GXT "TEMPGXT" vou ter o meu texto.
ADD_TEXT_LABEL (TEMPGXT) $pThreadMemory
// Apliquei algumas formatações de texto, para o texto aparecer mais bonito na tela.
SET_TEXT_CENTRE TRUE
SET_TEXT_CENTRE_SIZE 640.0
SET_TEXT_EDGE 1 (0 0 0 255)
// Mostrei a minha entrada GXT "TEMPGXT" na tela.
DISPLAY_TEXT (320.0 240.0) TEMPGXT
// Não podemos esquecer deste comando sempre que desenharmos coisas na tela em loop!
USE_TEXT_COMMANDS 0
GOTO main_loop
}
SCRIPT_END
Memory:
DUMP
// 32 Bytes
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ENDDUMPResultado:

Isso não seria possível no jogo sem utilizar o thread memory, pois precisamos de uma variável com mais de 16 bytes para criar este texto com a coordenada. Neste caso, utilizei de uma thread memory com 32 bytes, ou seja, posso escrever até 31 letras lá (sombrando 1 "00" como null terminator).
É claro, você pode personalizar o texto, por exemplo:
Código: Selecionar tudo
STRING_FORMAT (pThreadMemory) "X=%.3f Y=%.3f Z=%.3f" x y zPerceba que há um problema aqui:
Se você for numa coordenada MUITO longe, irá criar um número tão grande que ultrapassará os 32 bytes, assim sobrescrevendo o que está abaixo desta thread memory, e o que tem embaixo? No nosso código, nada, mas durante o gameplay, na sua memória RAM tem — sobrescreverá informações do jogo (acredito que informações desta ou outras threads), assim causando bugs e crashes, o chamado "stack overflow".
Variáveis adicionais
O jogo originalmente suporta somente 32 variáveis em threads externas (+2 timers). Mas tudo tem solução!
Eu nem preciso explicar pois logo no início desta parte você já viu isso, só lembre-se: você pode guardar informações no script sem usar variáveis!
Mas vamos para um exemplo prático:
Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT scplayer char chars_created i
LVAR_FLOAT x y z y_distance
GET_PLAYER_CHAR 0 scplayer
main_loop:
WAIT 0
IF TEST_CHEAT CREATE
y_distance = 1.0
chars_created = 0
WHILE chars_created < 40
GET_OFFSET_FROM_CHAR_IN_WORLD_COORDS scplayer (0.0 y_distance 0.0) (x y z)
CREATE_RANDOM_CHAR (x y z) (char)
CLEO_CALL StoreChar 0 (char, chars_created)
y_distance += 1.0
chars_created++
ENDWHILE
ENDIF
GOTO main_loop
}
SCRIPT_END
{
LVAR_INT char // In
LVAR_INT slot // In
LVAR_INT memory offset
StoreChar:
GET_LABEL_POINTER Memory memory
offset = slot * 4
memory += offset
WRITE_MEMORY memory 4 char FALSE
CLEO_RETURN 0
}
Memory:
DUMP
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
ENDDUMP
(usei o comando
CREATE_RANDOM_CHAR mas criou somente o MALE01.dff/txd pois apliquei o script sem esperar o jogo carregar os modelos de peds, veja aqui)Agora eu posso criar uma outra função que pega novamente os chars que eu guardei lá na memória, e assim controlá-los novamente:
Código: Selecionar tudo
SCRIPT_START
{
LVAR_INT scplayer char chars_created i
LVAR_FLOAT x y z y_distance
GET_PLAYER_CHAR 0 scplayer
main_loop:
WAIT 0
IF TEST_CHEAT CREATE
y_distance = 1.0
chars_created = 0
WHILE chars_created < 40
GET_OFFSET_FROM_CHAR_IN_WORLD_COORDS scplayer (0.0 y_distance 0.0) (x y z)
CREATE_RANDOM_CHAR (x y z) (char)
CLEO_CALL StoreChar 0 (char, chars_created)
y_distance += 1.0
chars_created++
ENDWHILE
ENDIF
IF TEST_CHEAT APPLY
REPEAT 40 i
CLEO_CALL GetChar 0 (i)(char)
TASK_KILL_CHAR_ON_FOOT char scplayer
ENDREPEAT
ENDIF
GOTO main_loop
}
SCRIPT_END
{
LVAR_INT char // In
LVAR_INT slot // In
LVAR_INT memory offset
StoreChar:
GET_LABEL_POINTER Memory memory
offset = slot * 4
memory += offset
WRITE_MEMORY memory 4 char FALSE
CLEO_RETURN 0
}
{
LVAR_INT slot // In
LVAR_INT memory offset char
GetChar:
GET_LABEL_POINTER Memory memory
offset = slot * 4
memory += offset
READ_MEMORY memory 4 FALSE (char)
CLEO_RETURN 0 (char)
}
Memory:
DUMP
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
ENDDUMPAcredito que o CJ não gostou muito do exemplo...

Mas vamos entender o que diabos estamos fazendo...
Quando eu digito
CREATE o script entra em um loop criando e guardando 40 chars na memória.Os chars são guardados na memória por uma função super simples:
Código: Selecionar tudo
{ // CLEO_CALL StoreChar 0 (char_handle, slot_id)()
LVAR_INT char // In
LVAR_INT slot // In
LVAR_INT memory offset
StoreChar:
GET_LABEL_POINTER Memory memory
offset = slot * 4
memory += offset
WRITE_MEMORY memory 4 char FALSE
CLEO_RETURN 0
}É fácil entender. Perceba que o
4 é a quantidade de bytes para cada slot (cada "espaço").Código: Selecionar tudo
offset = slot * 40, fará 0 * 4, ou seja, guardará em memory + 0, ou seja, nos primeiros bytes da minha thread memory (na primeira linha);Se estou guardando no slot
1, fará 1 * 4, ou seja, o offset será 4, portanto guardará em memory + 4, ou seja, na nossa segunda linha;Se estou guardando no slot
2, fará 2 * 4, ou seja, memory + 8, ou seja, na terceira linha de bytes.É um sistema super simples de implementar!
Para pegar o char devolta, é só fazer a exata mesma coisa, mas, ao invés de escrever, vamos ler e retornar o valor:
Código: Selecionar tudo
{ // CLEO_CALL GetChar 0 (slot_id)(char_handle)
LVAR_INT slot // In
LVAR_INT memory offset char
GetChar:
GET_LABEL_POINTER Memory memory
offset = slot * 4
memory += offset
READ_MEMORY memory 4 FALSE (char)
CLEO_RETURN 0 (char)
}Viu o que conseguimos fazer aqui? Nós criamos e temos o controle de 40 chars, sem gastar variáveis! Todos eles estão seguros, guardados na memória, onde a qualquer momento você pode ir lá pegá-los.
Valores compartilhados globalmente
Em mods grandes, é muito comum isso ser usado.
A explicação é simples: Os valores estão guardados na memória, e você tem acesso ao endereço daquela label facilmente com
GET_LABEL_POINTER. Você pode ler e escrever valores lá de onde você estiver.Um dos exemplos é o meu mod MDPMv5, onde eu utilizo vários scripts que se comunicam entre si... ...por thread memory!
Eu utilizo um script chamado por mim de "raiz", onde lá você ativa, e quando ativa, o carro é guardado na memória (para no futuro eu saber se aquele carro já está ativo ou não), e um novo script diferente é chamado. No novo script, eu recebo o endereço da thread memory onde está guardado todos os carros ativos. Quando eu desativo um tal carro, o script do carro remove ele mesmo da memória, assim liberando espaço para um novo carro e dizendo para o script raiz que o carro não está mais ativado e poderá ser ativado novamente.
Também neste script raiz eu armazeno informações globais, como qual é o atual carro que você está com o "controle remoto" selecionado, e cada script de cada carro também tem acesso à mesma thread memory com este valor lá para saber se o atual "controle remoto" é o tal carro ou não.
O MDPMv5 é um exemplo tão bom, que, para você ter noção, eu uso até thread memory para guardar ponteiros para outras thread memories. É louco.
Nota: o mesmo poderia ser feito pelas chamadas "CLEO VARS" (comandosSET_CLEO_SHARED_VAReGET_CLEO_SHARED_VAR) mas elas têm um limite de 1000 e podem causar conflitos entre mods que também usam, portanto eu não gosto de usá-las. O lado bom delas é que elas são armazenadas junto com o jogo salvo, isto sim pode ser um pouco útil, pois você pode guardar informações do seu mod no jogo salvo.
Também há outros modos, como usar manipulamento de memória para um script ler as variáveis do outro (quando estamos falando de manipulamento de memória, tudo é possível, meu jovem). Pra você ter noção, funções do MixSets editam os scripts do main.scm in-game para alterar o funcionamento dele, trocando os comandos, tudo isso comWRITE_MEMORY!
E não só entre scripts, mas, simplesmente,
CLEO_CALLs, diferentes escopos etc.E como sempre, eu tenho exemplos:
Em todos os mods que eu já fiz onde são multilíngues, eu fiz do mesmo modo:
Tenho uma função que lê o idioma configurado no arquivo .ini:
Código: Selecionar tudo
{
LVAR_INT iTempVar1 iTempVar2
CONST_INT LANG_EN 1 // English
CONST_INT LANG_PT 2 // Portuguese
CONST_INT LANG_ES 3 // Spanish
StoreLanguage:
IF NOT READ_INT_FROM_INI_FILE ("CLEO\MDPM.ini" "Settings" "Language") iTempVar1
OR iTempVar1 > LANG_ES
iTempVar1 = LANG_EN
ENDIF
GET_LABEL_POINTER Language iTempVar2
WRITE_MEMORY iTempVar2 1 iTempVar1 FALSE
CLEO_RETURN 0
IsEnglishLanguage:
GET_LABEL_POINTER Language iTempVar1
READ_MEMORY iTempVar1 1 FALSE iTempVar1
IF iTempVar1 = LANG_EN
RETURN_TRUE
ELSE
RETURN_FALSE
ENDIF
CLEO_RETURN 0
IsPortugueseLanguage:
GET_LABEL_POINTER Language iTempVar1
READ_MEMORY iTempVar1 1 FALSE iTempVar1
IF iTempVar1 = LANG_PT
RETURN_TRUE
ELSE
RETURN_FALSE
ENDIF
CLEO_RETURN 0
Language:
DUMP
00
ENDUMP
}Assim, basta eu simplesmente chamar esta função com um IF para checar se é o tal idioma:
Código: Selecionar tudo
IF CLEO_CALL IsEnglishLanguage 0
PRINT_STRING_NOW "Hi" 1000
ELSE
IF CLEO_CALL IsPortugueseLanguage 0
PRINT_STRING_NOW "Oi" 1000
ELSE // Spanish
PRINT_STRING_NOW "ARRIBA MUCHACHO!!!" 2000
ENDIF
ENDIFO ponto é: o valor ID do idioma está armazenado na memória, e não numa variável, portanto, eu posso fazer a checagem de idioma em absolutamente qualquer lugar do script!
Isto é muitíssimo bom para mods grandes, pois você não precisa ficar enviando uma variável com o idioma pra todo o lado, basta você chamar estas funções que elas pegarão o valor do idioma na memória.
Sinta-se livre!
Você pode criar Buffer, simular Class, criar Tables ou simplesmente guardar 1 byte de alguma informação para ser lido depois.
Em assunto de thread memory, você tem que se sentir totalmente livre.
Código: Selecionar tudo
Buffer16Bytes:
DUMP
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ENDDUMP
Language:
DUMP
00
ENDDUMP
IniValues:
DUMP
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
ENDDUMPEntenda o que temos aqui:
Nós temos, não só um espaço para armazenar informações, mas um código escrito em hexadecimal dentro do nosso script! (simplesmente, vários bytes de dados).
Você pode até mesmo pegar todo o hexadecimal de um arquivo de áudio e adicionar dentro de um
DUMP...ENDDUMP, quando necessário, você pode fazer seu script criar um novo arquivo e escrever todo o conteúdo daquele thread memory dentro do arquivo que você criou. Pronto! Seu script criou um arquivo de áudio! É como se você tivesse acoplado um arquivo dentro do seu .cs.Mas uma das coisas mais fodas que podemos fazer é adicionar códigos de outras programações dentro do seu script cleo, e neste caso, o Fabio nos fez este tutorial, onde inclui um auxiliador criado por ele: http://brmodstudio.forumeiros.com/t4721 ... s-cleo-scm
Veja por exemplo o script do mod Dynamic Tram Signs do Wesser (escrito no Sanny Builder, mas em GTA3script ficaria quase igual):
Você pode até criar scripts inteiros com thread memory. Na real, se você pegar todo o hexadecimal de um arquivo .cs e colocar dentro de um novo script com nenhum comando, somente uma thread memory, irá funcionar!
Veja este exemplo não muito útil, mas interessante:
Código: Selecionar tudo
SCRIPT_START
{
NOP
main_loop:
WAIT 0
IF IS_KEY_PRESSED VK_KEY_Y
DUMP
CD 0A 0E 08 55 6D 20 70 61 74 6F 2E 05 88 13
ENDDUMP
ENDIF
GOTO main_loop
}
SCRIPT_ENDCódigo: Selecionar tudo
CD 0A 0E 08 55 6D 20 70 61 74 6F 2E 05 88 13Código: Selecionar tudo
PRINT_STRING_NOW "Um pato." 5000Veja:
CD 0A = Opcode
0ACD que indica o PRINT_STRING_NOW (quem mexe com Sanny Builder está acostumado com isso! Veja)0E = Data type do próximo argumento ("long string")
08 = Quantidade de bytes da nossa long string.
55 6D 20 70 61 74 6F 2E = Caracteres da string
Um pato. (perceba que não tem null terminator (00) pois quem diz a quantidade de bytes da string é o 08 acima, portanto, null terminator aqui seria inútil.05 = Data type do próximo argumento ("integer")
88 13 = Valor
5000 — Experimente colocar 13 88 na calculadora do Windows no modo "Programador" com HEX (hexadecimal) selecionado e olhe no DEC (decimal), você verá o valor 5000! Lembrando que 88 13 é ao contrário pois a leitura e escrita de dados na memória é inversa.Perceba que nós começamos este tutorial com uma coisa super-ultra-extremamente simples, e olha até onde chegamos...
Thread memory vai desde uma "variável extra" a até acoplar arquivos e códigos de outra programação dentro do seu script.
Thread memory é uma coisa que você provavelmente usará tanto em mods complexos, quanto em mods mais simples.
Próxima parte:
Classes