sábado, 25 de agosto de 2012

Aula 10 - Packets

Tutorial - Packets

Oooi, quanto tempo não é? 
Sei que tinha bastante gente esperando mais aulas mas andei realmente muito ocupado com meu servidor (OverDestiny). E hoje resolvi vir aqui dar esta aulas a vocês. Vai ser uma aula bem legal pra quem quer realmente aprender sobre este assunto.


O que são packets?

Packets são como "conversas" entre duas "pessoas". Uma "pessoa" é você (no caso o jogador) e a outra é o servidor (o programa que controla tudo). Quando você, pessoa 1, se mexe no jogo ou então mexe algum item ou outra ação, o client manda um packet (a "fala") para a pessoa 2 dizendo o que você fez.

Seria assim:
Client -> Mexe item do slot 54 para o slot 55 (do inventário) -> Packet Enviado 
Servidor -> Recebe o packet -> Interpreta-o e verifica se é possível fazer esta ação -> Manda um packet de resposta (geralmente toda a pergunta tem uma resposta) 
Client -> Interpreta-o e faz você ver o que aconteceu.


Como ver um packet?

Para poder ver o packet você deve ter um packetcontrol na sua source. Um exemplo disso é:
for(int i=0;iSize;i++)
printf("%02X ", m_PacketBuffer[i]);
O que isso aí irá fazer?
Irá mostrar todos os bytes do packet recebido. Caso queira ver melhor, use:
printf("Recv ou Send -> Packet: 0x%X Size : %d ClientId: %d\n",packetHeader->PacketId,packetHeader->Size,packetHeader->ClientId);
printf("     ");
for(int i=0;iSize;i++)
{
printf("%02X ", m_PacketBuffer[i]);
if(i % 15 == 0 && i != 0)
printf("\n     ");
if(i >= packetHeader->Size)
printf("\n");
}


O que são os Bytes?

Os bytes são as informações que o packet carrega. Os primeiros 2 bytes (tipo short) é o tamanho do packet.

ATENÇÃO: Os bytes vão estar sempre em hexadecimal.
ATENÇÃO: Todo os bytes vem invertidos

Os bytes 2 e 3 são a Key e o Checksum. Não importantes agora para quem mexe com a TMSRV pois o SendPacket faz isso automaticamente.

Os bytes 4 e 5 é o ID do Packet, ou seja, sua identificação. Cada packet de ação tem seu próprio identificador. Alguns exemplos:
0x364 -> SendSpawn (envia um personagem para o client)
0x366 -> SendScore (atualiza alguns status do personagem)
0x333 -> SendSay (fala dos personagens)
0x399 -> ChangePK (quando personagem ativa/desativa o PK)
0x39F -> RequestDuel (pedido para duelo)

Esses aí são alguns. O WYD tem diversos packets que compõe o jogo. E você deve conhecer pelo menos os principais se quiser realmente programar!

Os bytes 6 e 7 é o ClientID. O ClientID é o identificar de quem está mandando o packet. 

Os bytes 8, 9, 10 e 11 é o TimeStamp, ou seja, o clock de quando foi enviado/recebido.

Todo o packet tem no mínimo 12 bytes (0~11 pois sua contagem começa de 0). Só existe um no jogo que tem apenas 4 bytes e não é encryptado que é o Hello Packet. Ele é enviado juntamente com o packet de login. 


O que é encryptação e decryptação?

Seus nomes são autoexplicativos. Encryptação nada mais é do que a forma em que foi encryptado o packet do jogo. Todo o packet, com exceção do HelloPacket, é encryptado. Antes de ser enviado para o servidor/client, o packet montado passa por uma função que faz ele ser encryptado (irreconhecível caso você não use um decryptador). Esse packet então é recebido pelo servidor ou client e então ele decrypta ele, neste caso ele usará o byte 2 e 3 para a decryptação pois mostrará como ele foi decryptado.


Exemplo de um packet

14 00 CB 3C 8B 02 01 00 F1 D4 62 05 99 10 00 00 01 00 00 00
Este é um exemplo de packet. Seguindo o que aprendemos sobre a ordem dos bytes temos:
0 e 1 = 14 00 -> 00 14 (invertidos, lembra?) = 20 = Size
2 = CB (apenas um byte) = 203 = Key
3 = 3C (apenas um byte) = 60 = CheckSum do client
4 e 5 = 8B 02 = 02 8B = 0x28B = PacketId
6 e 7 = 01 00 = 00 01 = 1 = ClientID
O resto não é importante agora.

Que packet é este? 
Este é o packet de quando você clica em algum NPC. É ele que iremos montar a struct como exemplo e para isto teremos que nos fazer algumas perguntas que eu faço:
O packet tem alguma relação com itens?
Tem alguma relação com boolean (true e false)?
Utiliza NPCs?

São perguntas bem simples mas que ajudam muito na hora de fazer a estrutura. Porque?
Quando você sabe que utiliza algum NPC, você já vai saber que lá pelo meio existirá o clientid deste mob em análise
Quando tem relação com um item, o ID dele ou o slot aparecerá no packet.
Quando tem uma relação com boolean você por exemplo aceitou o trade ou não ou clicou sim / não na janela de confirmação.


Informações de uma struct

Algumas coisas que deve saber é sobre o tamanho da variável. Ela influência TOTALMENTE na estrutura porque ela define quantos bytes você irá pegar. Os tipos básicos são:
BYTE = char = 1 Byte
WORD = short = 2 Bytes
DWORD = int = 4 Bytes

Acima disso não é usado em structs do WYD.  Caso defina algum tamanho errado, irá pegar provavelmente o valor errado (depende da struct).
A estrutura do PacketHeader (que é os 12 primeiros bytes que vimos anteriormente é assim):
typedef struct
{
WORD Size; // 2 bytes para o tamanho

BYTE Key; // 1 byte para a Key 
BYTE CheckSum; // 1 Byte para o CheckSum

WORD PacketId; // 2 Bytes para o PacketId (identificador do packet)
WORD ClientId; // 2 Bytes para o ClientID (quem manda o packet)

DWORD TimeStamp; // 4 Bytes para o clock ();
} PacketHeader;

Se o WORD Size fosse trocado para DWORD Size a estrutura retornaria valores totalmente errados. Então o que você deve fazer é prestar muito a atenção nas variáveis.

Downloads necessários: 

WydDecrypter -> usado para decryptar o packet caso não use o PacketControl - créditos: Petruka
WPEPro -> Pegar packets caso não use o PacketControl - site oficial

E claro, precisará de uma source. 

Atenção: não ensinarei a utilizar o WPEPro.

Pegando o packet para criar a estrutura

Bom, nosso primeiro packet será o 0x28B, que é quando você clica em algum NPC.
Ligue seu servidor normalmente, vá no jogo e pare no Mestre Arch (npc Skill BM).
Crie o item 1742 (Imortalidade) e ponha no slot dela
Abra o WPEPro, selecione o processo do WYD e aperte Play e clique no NPC e dê stop.
Um dos packets será do tipo Send. É ele que você deve procurar.

Após achar algum tipo Send, abra o WydDecrypter. Copie o packet e cole no console e aperte enter. Nele aparecerá o packet decryptado. Se você ver que os bytes 4 e 5 são 8B 02 está correto.

Copie-o para algum lugar (escrevendo manualmente) para efetuar a análise.

Análise do Packet

O que fizemos antes foi pegar o packet e o que faremos agora é analisá-lo. 
Os 12 primeiros bytes já sabemos o que é, não é mesmo? Então começaremos assim:
typedef struct {
PacketHeader Header;
} p28B;

PacketHeader já vem incluso na sua source, então basta adicioná-lo. 

É a partir disto que começaremos nossa analise. Os bytes que peguei foram:
14 00 CB 3C 8B 02 01 00 F1 D4 62 05 99 10 00 00 01 00 00 00
Até o 05 são 11 bytes, então até o 05 é o PacketHeader. É depois disso que iremos analisar.
Faremos o seguinte: iremos ver a partir dele como um byte, dois bytes e 4 bytes. Assim:
1 byte = 99hex = 153 decimal
2 bytes = 99 10 hex = 10 99 = 4249 decimal
4 bytes = 99 10 00 = 00 10 99  = 4249 decimal

Pensando assim... pelos conceitos básicos, o clientid de um personagem/npc varia de 1~30.000. E uma das perguntas que disse que deveria ser feita era:
Utiliza NPCs?
E aí? Utiliza? Ele está dentro do padrão de varição de clientids? Sim. 
Claro que em alguns packets não teremos certeza disso, então, isso apenas nossa estrutura teste, depois iremos testar ela e ver se está tudo ok.

Mas qual utilizar? O tamanho 2 bytes ou 4 bytes? 
O ClientID sempre será um valor 2 bytes porque nenhum client ultrapassará 2 bytes. Então temos uma variável tipo short, certo? 
Adicione na struct com o nome que quiser abaixo do PacketHeader, assim:
typedef struct {
PacketHeader Header;
short MobID;
} p28B;

Agora os 4 próximos bytes são: 00 00 01 00 
1 Byte = 00 hexadecimal = 0 decimal
2 Bytes = 00 00 hexadecimal = 0 decimal
4 Bytes = 00 00 01 00 = 65536 decimal

Pensando assim... o valor de 4 bytes é um valor muito alto. O de 2 bytes é igual a de 1 byte... Então tá aí o que não poderemos descobrir já. Esses bytes em que aparecem 0 geralmente colocamos como Unknow (desconhecido). Como são 2 bytes desconhecidos para ocuparmos menos linhas utilizaremos um short, ficando:
typedef struct {
PacketHeader Header;
short MobID;
short Unknow1;
} p28B;

Atenção: Nem todo o packet que retorna 00 00 ou 00 ou 00 00 00 00 será um Unknow (desconhecido). Tudo depende do que você está fazendo. Existem packets que sim, retornam 0 e que você deve identificar porque de alguma outra forma ele retornará 1, como no caso do próximo

Os 4 próximos bytes são  01 00 00 00 
1 Byte = 01 hexadecimal = 1 decimal
2 Bytes = 01 00 hexadecimal = 1 decimal
4 Bytes = 01 00 00 00 hexadecimal = 1 decimal

Esses valores todos retornam igual, mas você terá que identificar o que ele é. Lembra-se disso?
Tem alguma relação com boolean (true e false)?
Sim, ele tem. E ele é que você clica OK quando o packet é enviado. Se você clicar em um NPC sem a confirmação esse valor virá 0.

Para ter certeza, entre no jogo e faça o mesmo processo do WPE, clique no NPC que não tem confirmação e que não seja loja, faça o processo do WYDdecryptar e veja o resultado.

Mas qual utilizar? 2 Bytes ou 4 Bytes?
Nesse caso, é melhor usarmos o 2 bytes. Porque se os próximos valores vierem alterados ele modifica o valor todo. 

Então ficando:
typedef struct {
PacketHeader Header;
short MobID;
short Unknow1;
short Clicked;
} p28B;

Os últimos 2 bytes são: 00 00.
Você fez mais algo pra acontecer? Não? Está aí mais um Unknow. Apenas adicione-o como:
short Uknow2, ficando:
typedef struct {
PacketHeader Header;
short MobID;
short Unknow1;
short Clicked;
short Unkow2;
} p28B;

Uma estrutura simples. Para testar, coloque no seu PacketControl o case 0x28B (id do packet) puxando as informações do packet da seguinte forma:
NomeDaEstrutura *p = (NomeDaEstrutura*)m_PacketBuffer;

Acesse-o usando: p->

Finalizando
Bom, tentei explicar do jeito mais simples. Espero que gostem (;

domingo, 1 de julho de 2012

[Função] Colocar itens no banco e atualizá-lo

Bom, tá aí uma função que poe um item no banco e atualiza ele.

SetItemBank
void SetItemBank(int clientid, int slot, st_Item item) {
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C) = item.Index;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x2) = item.EF1;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x3) = item.EFV1;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x4) = item.EF2;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x5) = item.EFV2;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x6) = item.EF3;
PInt(0x7B31A9C + (slot * 8) + clientid * 0xC4C + 0x7) = item.EFV3;
}

Packet 0x182:
struct pCL_182h
{
    PacketHeader Header;
    short invType;
    short invSlot;
    st_Item itemData;
};


void p182(int clientid, int type, int slot, st_Item item) {
pCL_182h p;
p.Header.ClientId = clientid;
p.Header.Size = sizeof pCL_182h;
p.Header.PacketId = 0x182;
p.invType = type;
p.invSlot = slot;
memcpy(&p.itemData, &item, sizeof st_Item);
SendPacket((BYTE*)&p, clientid, sizeof pCL_182h);
}
Adicione o define:
#define BancoSlot(clientid, slot) *(unsigned short*)(0x7B31A9C + (slot * 8) + clientid * 0xC4C)


Agora adicione a função:
int GetFirstSlotInBank(int clientid, int item)
{
st_Mob * player = (st_Mob*)GetMobFromIndex(clientid);
int cSlot; int slot = -1;
for(int i = 0;i < 128;i++)
{
cSlot = BancoSlot(clientid, i);
if(cSlot == item)
{
slot = i;
break;
}
}
return slot;
}


Para usar basta:
st_Item item;
item.Index = 5138;
p182(clientid, 2, GetFirstSlotInBank(clientid, 0), item);

Em item você bota todas as informações do item.


Créditos:
shepher

terça-feira, 5 de junho de 2012

Aula 7.2- Merchants e Informações

Aula 7.2 - Merchants e Informações
Oi galera, quanto tempo, ein? Ultimamente não ando mais postando nada. Pois é. Ando consumindo meu tempo com o OverDestiny e isso tá tenebroso, haha. Mas como amanhã tenho que ir pra aula somente 8 e meia, vou escrever essa aula extra.

Iremos aprender basicamente a como configurar o NPC corretamente para funcionar dentro do jogo.
OBS: A aula não vai ser extensa, e talvez eu passe mais alguns comandos posteriormente para assim concluirmos com packets.

Toda a release possui o programa chamado EDITAPPMOB ou EDITMOB ou qualquer outra coisa do gênero. Esses programas são especificadamente para editar tudo sobre os npcs. 
Para poder abri-lo corretamente, deverá conter na sua pasta os arquivos: Itemeffect.h , itemlist.csv e a pasta npc. Os dois arquivos é de onde ele pega as informações dos itens, e pasta NPC é toda a informação de todos os NPCs.

Abra-o e aparecerá algo parecido com isto:
W.Y.D Mob Editer 4.10
Adicionei nas partes mais importantes. O resto é autoexplicativo.
  • 1 - É o tipo de mob. São 127 tipos, sendo muitos repetidos e sem funções. Por exemplo: 1 é mob para atacar. 3 é para lojas e assim vai indo. Tem as races para cada coisinha que você vê no BR, como por exemplo, as janelinhas e etc.
  • 2 - Merchant é o tipo 2 do mob. Se você colocar 3 como race e 19 como merchant terá o NPC de skill. Se você colocar 3 como race e 1 como merchant terá as lojas.
  • 3 - É o que na dll que postei defini o que cada um faz na source. Você vai configurar assim como nas aulas 7 e criar o MOB com o Level configurado lá.
  • 4 5 6 - Todo o inventário do mob. (não sei nem pra que botei na imagem, mas né).
Muita gente veio me perguntar no tópico ou no msn qual a merchant e a race que eu uso. Bom, para fim de conversa é 8 e 6 respectivamente. 
Para NPCs de confirmação uso 58 e 3.

Finalizando
Pois é. Isso não foi bem uma aula. Mas enfim, tá aí, só pra tirarem alguns dúvidas. 
Atenciosamente, 
Shepher

sexta-feira, 25 de maio de 2012

AutoConfigure Server - Utilitário


Oie gente,

tava sem nada pa fazer... E eu tinha que configurar um servidor 6.56. A preguiça tomou conta, porque eu mudo muito o IP também. E eu fiz esse utilitário que configura o servidor automaticamente de acordo com o IP digitado.



Atenção: as pastas devem estar com o seguinte nome:
DBsrv e TMsrv.

O sistema é case-sensitive. Se tiver diferença nas maiúsculas e minúsculas, não vai funcionar.

Download:
Retirado, abaixo.


ATUALIZAÇÃO
Novo link com correçaão de bug:
Download 1.1

Ele é bem simples, fiz em 3 min ._..

Qualquer coisa, avisem.

Espero que usem, e gostem.


Créditos:
shepher.

Aula 9 - MobKilledEvent

Aula 9 - MobKilledEvent 
Uma aula exclusiva para a minha DLL, já que outras DLLs não são iguais. Então vamos a uma breve explicação do que é:
MobKilledEvent é uma função que acontece quando um mob específico morre. Pode ser qualquer MOB. Desde que você tenha os números do status dele. Todos os dados podem ser verificados, como nome, exp, level, learn, set, arma, e etc. Você pode ser bem específico caso você queira que não ocorra algum bug, como por exemplo ter um mob com o mesmo nome, só que esse mob seria um boss e se o servidor, por algum caso misterioso, "confundisse" os dois e identificasse como boss, seria um problema.

Eu deixei na DLL o seguinte código como principal:
void MobKilledEvent(BYTE *m_PacketBuffer)
{
p338 *p = (p338*)m_PacketBuffer; char tmp[1024];
WORD clientid = p->killer; WORD mobid = p->killed;
st_Mob *mob = (st_Mob*)GetMobFromIndex(p->killed);
MobKilledE *MobKilled = new MobKilledE();
if(strcmp((char*)mob->Name, "Kefra") == 0)
MobKilled->Kefra(clientid, mobid);
else if(strcmp((char*)mob->Name, "NOMEQUEQUISER") == 0)
return; // Função que quiser;
else {
sprintf(tmp, "Nome do mob é: %s", (char*)mob->Name);
SendClientMsg(clientid, tmp);
}
return;
}
Source OverDestiny  v1.1
Bom, o código é bem simples, não tem segredo. Deixei ali como exemplo para quando o personagem matar um mob com o nome "Kefra". .E como faço pra adicionar outro MOB pelo nome?

Simples, como tá mostrando ali, você só tem que adicionar o else if com uma comparação do nome do mob morto pelo que quiser. Ali tem um exemplo: "NOMEQUEQUISER".
else if(strcmp((char*)mob->Name, "NOMEQUEQUISER") == 0)
Você só trocaria o NOMEQUEQUISER pelo nome do Mob que quer que faça algo.
Caso queira mais um, adicione logo abaixo outro else if desse.

Além do nome, quero verificar outras coisas. Como faço?
Você simplesmente adiciona um else if com a condição que quiser. Caso queira pelo level, ficaria:
else if(mob->bStatus.Level == 500)
E logo após isso, a função.

Criando as Funções para o MobKilledEvent
Para criar uma função você deve declará-la, primeiramente. Será declarado no arquivo "MobKilledEvent.h" dentro da class MobKilledE.
Vamos fazer um exemplo. O código atual é:
/*
-------------
by Shepher
-------------
*/
#ifndef __MOBKILLED_EVENT_H__
#define __MOBKILLED_EVENT_H__

class MobKilledE{
public:
void Kefra(int clientid, int npcid);
};
#endif
Iremos criar a função para o mob chamado "Cav._Kaizen". O nome da func farei:
void MobKilledE::Cav_Kaizen(int clientid, int npcid)
Declare ela no MobKilledEvent.h assim:
void Cav_Kaizen(int clientid, int npcid);
Ficando o total no arquivo assim:
/*
-------------
by Shepher
-------------
*/
#ifndef __MOBKILLED_EVENT_H__
#define __MOBKILLED_EVENT_H__

class MobKilledE{
public:
void Kefra(int clientid, int npcid);
void Cav_Kaizen(int clientid, int npcid);
};
#endif

Entendeu como declara? Se não, volte o tutorial.
Após isso faça o bloco de códigos para a função:
void MobKilledE::Cav_Kaizen(int clientid, int npcid)
{
st_Mob *player = (st_Mob*)GetMobFromIndex(clientid); char tmp[1024];

}
Fazendo isso, faça o código que souber fazer normalmente. Como ensinamos nos códigos de NPCs e comandos.

Na função principal, você adicionará a condição para acionar o evento. Pensaremos numa legal. O mob tem que ter o nome citado anteriormente, level 355, exp 50000, okay? Então fica assim:
else if(strcmp((char*)mob->Name, "Cav. Kaizen") == 0 && mob->bStatus.Level == 354 && mob->Exp == 50000)
OBS: No nome deve-se trocar o underline ( "_" ) por espaço ( " " );

Após isso, só chamaremos a função usando MobKilled->. Ficando assim:
else if(strcmp((char*)mob->Name, "Cav. Kaizen") == 0 && mob->bStatus.Level == 354 && mob->Exp == 50000)
MobKilled->Cav_Kaizen(clientid, mobid);

Finalizando
Bom, hoje aprendemos a criar um novo mob para o MobKilledEvent. Espero que tenham gostado.:D


Então, só testar no jogo.


Atenciosamente,
Shepher

quarta-feira, 23 de maio de 2012

[Source]Catalisador Makav - Exemplo de SendRemoveMob

Um exemplo pra quem quer utilizar a Função SendRemoveMob e não sabe como.

Tá aí a Source para o Catalisador Makav.

void Catalisador_Makav(int clientid, int SrcSlot, int DstSlot, int warp)
{
st_Mob *player = (st_Mob*)GetMobFromIndex(clientid);
int mount = player->Equip[14].Index;
char tmp[1024];
int cHora = PInt(0x00A5EA418);
int cMin = PInt(0x00A5EA414);
int cSeg = PInt(0x00A5EA410);
srand(time(NULL) * (cHora / cMin + cSeg + ((cMin + rand() % 100 - cSeg) / cHora)) * rand()%200+150);
if(mount >= 2330 && mount <= 2358)
{
if(DstSlot == 14)
{
if(mount == 2354 || mount == 2355 || mount == 2356)
{
/*EF2 = level
EFV2 = vitalidade
EF3 = ração
EFV1 = HP*/
int mountIn = mount + 30;
player->Equip[14].Index = mountIn;
player->Equip[14].EFV2 += rand() % 16;
player->Equip[14].EF2 = 0;
SendItens(clientid, 14);
player->Inventory[SrcSlot].Index = 0;
SendRemoveMob(clientid);
SendAll(clientid);
return;
} else {
SendClientMsg(clientid, "Tipo não confere");
SendAll(clientid);
return;
}
} else {
SendClientMsg(clientid, "Utilize em montarias");
SendAll(clientid);
return;
}
} else {
SendClientMsg(clientid, "Aplicado somente em montarias");
SendAll(clientid);
return;
}
return;
}


Créditos:
Shepher;

terça-feira, 22 de maio de 2012

[Source]Todas as rações

Na TMSRV atual, algumas das rações, são bugadas impossibilitando usá-las. Então resolvi postar a primeira source de rações que eu fiz e que uso até hoje.
Ela tá bem grandinha. Poderia diminuir para ficar automático, mas algumas montarias não tem IDs certinhos (não sei explicar). Enfim,

tá aí.

void Racoes(int clientid, int SrcSlot, int DstSlot, int warp)
{
st_Mob *player = (st_Mob*)GetMobFromIndex(clientid);
int mount = player->Equip[14].Index;
if(player->Equip[14].Index >= 2360 && player->Equip[14].Index <= 2388)
{
int mountIndex;
if(mount == 2360) mountIndex = 0;
if(mount == 2361) mountIndex = 1;
if(mount == 2362) mountIndex = 2;
if(mount == 2363) mountIndex = 3;
if(mount == 2364) mountIndex = 4;
if(mount == 2365) mountIndex = 5;
if(mount >= 2366 && mount <= 2370) mountIndex = 6;
if(mount == 2371) mountIndex = 9;
if(mount >= 2372 && mount <= 2375 || mount == 2387) mountIndex = 6;
if(mount == 2376) mountIndex = 16;
if(mount == 2378) mountIndex = 18;
if(mount == 2371 || mount == 2381 || mount == 2382 || mount == 2383 || mount == 2388) 
mountIndex = 9;
if(mount == 2377) mountIndex = 17;
if(mount == 2380) mountIndex = 8;
if(mount >= 2384 && mount <= 2386) mountIndex = 10;
if((mountIndex + 2420) == player->Inventory[SrcSlot].Index)
{
/*EF2 = level
EFV2 = vitalidade
EF3 = ração
EFV1 = HP*/
int val;
if(player->Equip[14].EFV1 >= 120 || player->Equip[14].EFV1 == 0)
return;
val = player->Equip[14].EFV1 += 30;
if(val > 100)
val = 100;
player->Equip[14].EFV1 = val;
val = player->Equip[14].EF3 += 1;
if(val > 100)
val = 100;
player->Equip[14].EF3 = val;
AmountMinus(clientid, SrcSlot);
AtualizaInvt(clientid);
} else {
SendClientMsg(clientid, "Tipo não confere");
return;
}
SendItens(clientid, 14);
} else {
SendClientMsg(clientid, "Usado apenas em montarias");
return;
}
}


Créditos totais a Shepher + Supernov4 (lógica)