Análisis del Código de Movimiento y FPS en MUONLINE

Análisis del Código de Movimiento y FPS en MUONLINE

Introducción

El código presentado es parte del sistema de movimiento de cámara y física en MU Online, un popular juego de rol multijugador. A continuación, desglosamos las secciones más relevantes del código para entender su funcionamiento y cómo impacta en la experiencia del jugador.

1. Cameramove.h

Directivas de Preprocesador

  • #pragma once: Asegura que el archivo se incluya solo una vez durante la compilación.
  • #define: Define constantes y máscaras para diferentes propiedades físicas y de cámara, esenciales para el manejo de la jugabilidad.

Definiciones de Constantes

Se definen varios tipos de propiedades físicas y de cámara, como:

  • FormasPCT_FLATPCT_CURVED.
  • MaterialesPCT_COTTONPCT_RUBBER.

Variables Globales

Se definen punteros a direcciones de memoria específicas para variables como FPS, tiempos de delta, y tipos de cámara, que son cruciales para el rendimiento del juego.

Funciones

Se declaran funciones que interactúan con la física del juego, como mover la cámara y actualizar fuerzas físicas, lo que es vital para la jugabilidad fluida.

Estructura PhysicsVertex

Define una estructura que almacena información sobre un vértice físico, incluyendo:

  • Posición
  • Velocidad
  • Estado

Clase CCameraMove

Contiene métodos para inicializar la cámara, mover vértices físicos y reproducir animaciones 3D, mejorando la inmersión del jugador.

2. CameraMove.cpp

Inicialización

Se inicializan variables globales y se define el constructor y destructor de CCameraMove, asegurando que los recursos se gestionen correctamente.

Cálculo de FPS

La función CalcFPS actualiza el recuento de FPS y puede cambiar el título de la ventana del juego con información relevante, lo que ayuda a los jugadores a monitorear el rendimiento.

Funciones de Movimiento

  • CPhysicsCloth_InitForces y CPhysicsVertex_Move gestionan la física de los objetos en el juego, calculando fuerzas y actualizando posiciones basadas en el tiempo, lo que es esencial para el movimiento realista de personajes y objetos.

Renderizado de Personajes

Las funciones SpeedCharacterBackItem1, SpeedCharacterBackItem2, y SpeedCharacterBackItem3 ajustan la velocidad de los personajes en función de su tipo, mejorando la jugabilidad.

Animaciones

PlayAnimation3D gestiona la reproducción de animaciones en 3D, aplicando la velocidad de animación calculada, lo que contribuye a una experiencia visual más rica.

Método Init

Configura varios hooks y ajustes en el comportamiento del juego, como arreglos en la interfaz y el movimiento de la cámara, optimizando la experiencia del jugador.

3. Hackcheck.cpp

Chequeo de Hack

CheckTickCount2 verifica modificaciones no autorizadas en el juego, comparando varios modificadores y valores de tiempo para mantener la integridad del juego.

4. gProtect.cpp

Control de FPS

FT_Nine2D ajusta el límite de FPS basado en configuraciones predefinidas, asegurando una experiencia de juego fluida y sin interrupciones.

Cameramove.h


#pragma once
#define PVS_NORMAL                  ( 0x00)
#define PVS_FIXEDPOS               ( 0x01)
#define RATE_SHORT_SHOULDER            ( 0.6f)
#define PCT_MASK_SHAPE               ( 0x00000003)
#define PCT_FLAT                  ( 0x00000000)
#define PCT_CURVED                  ( 0x00000001)
#define PCT_STICKED                  ( 0x00000002)
#define PCT_MASK_SHAPE_EXT            ( 0x0000000C)
#define PCT_SHAPE_NORMAL            ( 0x00000000)
#define PCT_SHORT_SHOULDER            ( 0x00000004)
#define PCT_CYLINDER               ( 0x00000008)
#define PCT_MASK_SHAPE_EXT2            ( 0x00000030)
#define PCT_SHAPE_HALLOWEEN            ( 0x00000010)
#define PCT_MASK_ELASTIC            ( 0x00000300)
#define PCT_COTTON                  ( 0x00000000)
#define PCT_RUBBER                  ( 0x00000100)
#define PCT_RUBBER2                  ( 0x00000200)
#define PCT_MASK_WEIGHT               ( 0x00000C00)
#define PCT_NORMAL_THICKNESS         ( 0x00000000)
#define PCT_HEAVY                  ( 0x00000400)
#define PCT_MASK_DRAW               ( 0x00003000)
#define PCT_MASK_BLIT               ( 0x00000000)
#define PCT_MASK_ALPHA               ( 0x00001000)
#define PCT_MASK_BLEND               ( 0x00002000)

#define FPS_                     *(float*)0x5EF5A18
#define DelTtime                  *(float*)0xE61610
#define CameraWalkCut               *(int*)0xE8C83C
#define CurrentCameraCount            *(int*)0xE60AB0
#define CurrentCameraNumber            *(int*)0xE8CB48
#define CurrentCameraWalkType         *(int*)0xE8CB44
#define MoveSceneFrame               *(int*)0xE8CB20
#define s_fInvOfMass               *(float*)0xE614C8

#define listWayPoint_empty            ((BOOL(__thiscall*)(void* This)) 0x004DEC50)
#define listWayPoint_size            ((int(__thiscall*)(int This)) 0x004DEC30)
#define GetlistWayPoint               ((unsigned int (__thiscall*)(int This, unsigned int index)) 0x004DEC70)
#define sub_4DEB00                  ((int(__thiscall*)(int This)) 0x004DEB00)
#define CreateAngle                  ((double(__cdecl*)(float a1, float a2, float a3, float a4)) 0x00540C30)
#define absf                     ((double(__cdecl*)(float a1)) 0x00639050)
#define sub_959B10                  ((double(__cdecl*)(int This, int v2)) 0x00959B10)
#define MoveCharacterCamera            ((void(__cdecl*)(vec3_t Origin,vec3_t Position,vec3_t Angle)) 0x004D66C0)
#define UpdateForce                  ((void(__thiscall*)(int This, unsigned int iKey, DWORD dwType, float fWind)) 0x0051C890)
#define CPhysicsVertex_Init            ((void(__thiscall*)(int This, float fXPos, float fYPos, float fZPos, BOOL bFixed)) 0x0051C850)
#define sub_9CF1C0                  ((void(__cdecl*)(int a1, int a2, int a3)) 0x009CF1C0)
#define PlayAnimation               ((bool(__thiscall*)(int This, float *AnimationFrame,float *PriorAnimationFrame,unsigned short *PriorAction,float Speed,float * Origin,float * Angle)) 0x00545180)


typedef struct
{
   int     byClass;
   float   m_vForce[3];
   float   m_vVel[3];
   float   m_vPos[3];
   BYTE    m_byState;
   char    sobrante[16];
}PhysicsVertex, * CPhysicsVertex;

class CCameraMove
{
public:
   enum {
      CAMERAWALK_STATE_READY = 0,
      CAMERAWALK_STATE_MOVE,
      CAMERAWALK_STATE_DONE,
   };
   CCameraMove(void);
   virtual ~CCameraMove(void);
   void Init();

   static void __thiscall CPhysicsCloth_InitForces(int This);
   static void __thiscall CPhysicsVertex_Move(CPhysicsVertex This, float fTime);

   static bool __thiscall PlayAnimation3D(int This, float *AnimationFrame,float *PriorAnimationFrame,unsigned short *PriorAction,float Speed,float* Origin,float* Angle);

};

extern CCameraMove g_CameraWalkInstance;



CameraMove.cpp


#include "StdAfx.h"
#include "Util.h"
#include "Camera.h"
#include "Protect.h"
#include "SEASON3B.h"
#include "CustomMap.h"
#include "CameraMove.h"

int FpsCount = 0;
int FpsTime = 0;
int activespeedmove = 0;
int AnimationFrameConstant = 30;
CCameraMove g_CameraWalkInstance;

CCameraMove::CCameraMove(void)
{
}

CCameraMove::~CCameraMove(void)
{
}

void CalcFPS()
{
   ((void(__cdecl*)()) 0x00542EF0)(); FpsCount++;

   if(GetTickCount() >= (FpsTime + 999))
   {
      AnimationFrameConstant = FpsCount;

      /*char szTitle[254] = {'\0',};
      if(SceneFlag == MAIN_SCENE)
      {
         sprintf_s(szTitle, "%s [%s][Lvl: %d] - %s (%d %d) [FPS:%d]", gProtect->m_MainInfo.WindowName,
               (char*)(Hero + 56), *(WORD*)(*(DWORD*)0x8128AC8 + 14), GetMapName(World), *(int*)(Hero + 172), *(int*)(Hero + 176), FpsCount);
      }
      else if(SceneFlag == CHARACTER_SCENE)
      {
         sprintf_s(szTitle, "%s [SELECT CHARACTER SCENE] - [FPS: %d]", gProtect->m_MainInfo.WindowName,  FpsCount);
      }
      else if(SceneFlag == LOG_IN_SCENE)
      {
         sprintf_s(szTitle, "%s [SERVER LOG IN SCENE] - [FPS: %d]", gProtect->m_MainInfo.WindowName,  FpsCount);
      }

      if( szTitle[0] != '\0')
      {
         SetWindowText(g_hWnd, szTitle);
      }*/

      FpsTime = GetTickCount(); FpsCount = 0;
   }

   gCamera.ZoomInFactor();

   gCamera.RotateInFactor();
}

void AnimationSpeed(float* Speed)
{
   (*Speed) *= ((AnimationFrameConstant > 25) ? (1.f / (AnimationFrameConstant / 25.f)):1.f);
}

void RenderCharacterBackItem(int w, int o, int Type)
{
   float PlaySpeed = 1.f;

   AnimationSpeed(&PlaySpeed);

   if( Type == 1 )
   {
      if ( *(WORD *)(o + 18) != 34 && *(WORD *)(o + 18) != 35 )
         *(float *)(w + 20) = 0.25 * PlaySpeed;
      else
         *(float *)(w + 20) = 1.0 * PlaySpeed;
   }

   if( Type == 2 )
   {
      if ( *(WORD *)(o + 18) != 34 && *(WORD *)(o + 18) != 35 )
         *(float *)(w + 20) = 0.25 * PlaySpeed;
      else
         *(float *)(w + 20) = 1.0 * PlaySpeed;
   }

   if( Type == 3 )
   {
      if ( *(WORD *)(o + 18) != 34 && *(WORD *)(o + 18) != 35 )
      {
         *(float *)(w + 20) = 0.25 * PlaySpeed;
      }
      else if ( *(WORD *)w == 7351 )
      {
         *(float *)(w + 20) = 0.5 * PlaySpeed;
      }
      else
      {
         *(float *)(w + 20) = 1.0 * PlaySpeed;
      }
   }
}

void CCameraMove::CPhysicsCloth_InitForces(int This)
{
   int m_iNumVertices = *(int *)(This + 52);
   int m_iNumHor      = *(int *)(This + 44);
   int m_pVertices    = *(int *)(This + 56);

   int iSeed = ((MoveSceneFrame / 10) * 101) % m_iNumVertices;

   int o = *(int *)(This + 4);
   int Type = *(int *)(o + 48);
   WORD CurrentAction = *(WORD*)(o + 18);

   activespeedmove = 0;

   if(Type == 1163 &&
      ((CurrentAction >= 15  //-- PLAYER_WALK_MALE
      && CurrentAction <= 24)  //-- PLAYER_WALK_SWIM
      || (CurrentAction >= 34  //-- PLAYER_FLY
      && CurrentAction <= 37)  //-- PLAYER_RUN_RIDE_WEAPON
      || CurrentAction == 74   //-- PLAYER_FLY_RIDE
      || CurrentAction == 75   //-- PLAYER_FLY_RIDE_WEAPON
      || CurrentAction == 77   //-- PLAYER_DARKLORD_WALK
      || CurrentAction == 79   //-- PLAYER_RUN_RIDE_HORSE
      || (CurrentAction >= 110  //-- PLAYER_FENRIR_RUN
      && CurrentAction <= 121)  //-- PLAYER_FENRIR_RUN
      || (CurrentAction >= 126  //-- PLAYER_FENRIR_WALK
      && CurrentAction <= 129)  //-- PLAYER_FENRIR_WALK
      || CurrentAction == 143   //-- PLAYER_WALK_TWO_HAND_SWORD_TWO
      || CurrentAction == 144   //-- PLAYER_RUN_TWO_HAND_SWORD_TWO
      ))
   {
      activespeedmove = 1;
   }

   for ( int iVertex = 0; iVertex < m_iNumVertices; ++iVertex)
   {
      float m_fWind = *(float *)(This + 68);
      int m_dwType  = *(DWORD *)(This + 20);

      UpdateForce(m_pVertices + iVertex * 60, abs( iSeed % m_iNumHor - iVertex % m_iNumHor) + abs( iSeed / m_iNumHor - iVertex / m_iNumHor), m_dwType, m_fWind );
   }
}

void CCameraMove::CPhysicsVertex_Move(CPhysicsVertex This, float fTime)
{
   float fSpeed = 1.f;
   float Time = fTime;

   AnimationSpeed(&fSpeed);

   if(AnimationFrameConstant > 25)
   {
      Time *= fSpeed;
   }

   if ( !(This->m_byState & PVS_FIXEDPOS) && activespeedmove == FALSE)
   {
      for (int i = 0; i < 3; ++i )
      {
         This->m_vVel += (This->m_vForce * s_fInvOfMass * fTime) * fSpeed;
         This->m_vPos += (This->m_vVel * Time);
      }
   }
}

void __declspec(naked) SpeedCharacterBackItem1()
{
   static DWORD JmpBack = 0x005882A5;

   _asm
   {
      push    1
      mov     edx, dword ptr ss:[ebp+0xC]
      push    edx
      mov     eax, dword ptr ss:[ebp-0x20]
      push    eax
      call    [RenderCharacterBackItem]
      Jmp     JmpBack
   }
}

void __declspec(naked) SpeedCharacterBackItem2()
{
   static DWORD JmpBack = 0x0058848E;

   _asm
   {
      push    2
      mov     edx, dword ptr ss:[ebp+0xC]
      push    edx
      mov     eax, dword ptr ss:[ebp-0x5C]
      push    eax
      call    [RenderCharacterBackItem]
      Jmp     JmpBack
   }
}

void __declspec(naked) SpeedCharacterBackItem3()
{
   static DWORD JmpBack = 0x005885AA;

   _asm
   {
      push    3
      mov     eax, dword ptr ss:[ebp+0xC]
      push    eax
      mov     ecx, dword ptr ss:[ebp-0x6C]
      push    ecx
      call    [RenderCharacterBackItem]
      Jmp     JmpBack
   }
}

bool CCameraMove::PlayAnimation3D(int This, float *AnimationFrame,float *PriorAnimationFrame,unsigned short *PriorAction,float Speed,float * Origin,float * Angle)
{
   AnimationSpeed(&Speed);

   return PlayAnimation(This, AnimationFrame, PriorAnimationFrame, PriorAction, Speed, Origin, Angle);
}

void CCameraMove::Init()
{
   SetByte(0x00813BF1 + 3, 1); //-- Fix Tooltip SkillBar
   SetByte(0x004EBD01 + 6, 1); //-- Fix Tooltip Ancient
   //SetCompleteHook(0xE9, 0x005977E0, &RenderNotices);
   SetCompleteHook(0xE8, 0x004D9D0E, &CalcFPS); //ok
   SetByte(0x0059788E + 1, 0x14); //-- Fix Notice Midd
   SetByte(0x00597895 + 2, 0xA); //-- Fix Notice Midd
   SetCompleteHook(0xE9, 0x00588277, &SpeedCharacterBackItem1);
   SetCompleteHook(0xE9, 0x00588460, &SpeedCharacterBackItem2);
   SetCompleteHook(0xE9, 0x00588560, &SpeedCharacterBackItem3);
   SetCompleteHook(0xE8, 0x004F0356, &CCameraMove::PlayAnimation3D);
   SetCompleteHook(0xE9, 0x0051CB60, &CCameraMove::CPhysicsVertex_Move);
   SetCompleteHook(0xE9, 0x0051E440, &CCameraMove::CPhysicsCloth_InitForces);;
}


Hackcheck.cpp


_declspec(naked) void CheckTickCount2() // OK
{
   static DWORD CheckTickCountAddress1 = 0x004DA3F0;
   static DWORD FrameCount = 0x11;

   if(*(int*)0xE609E8 == 5)
   {
      FrameCount = gProtect->FT_Nine2D();
   }
   else
   {
      FrameCount = 0x28;
   }

   _asm
   {
      Mov Ecx,Dword Ptr Ss:[Ebp-0x6C]
      Mov CountModifier,Ecx
      Mov Edx,Dword Ptr Ss:[Ebp-0x74]
      Mov DelayModifier,Edx
      Mov Ecx,Dword Ptr Ss:[Ebp-0x178]
      Mov HasteModifier,Ecx
      Mov Edx,Dword Ptr Ds:[MAIN_VIEWPORT_STRUCT]
      Mov Ecx,Dword Ptr Ds:[Edx+0x214]
      Mov SpeedModifier1,Ecx
      Mov Edx,Dword Ptr Ds:[MAIN_VIEWPORT_STRUCT]
      Mov Ecx,Dword Ptr Ds:[Edx+0x218]
      Mov SpeedModifier2,Ecx
      Mov Edx,Dword Ptr Ds:[MAIN_VIEWPORT_STRUCT]
      Mov Ecx,Dword Ptr Ds:[Edx+0x31A]
      Mov ModelModifier1,Ecx
      Mov Edx,Dword Ptr Ds:[MAIN_VIEWPORT_STRUCT]
      Mov Ecx,Dword Ptr Ds:[Edx+0x394]
      Mov ModelModifier2,Ecx
      Mov Edx,Dword Ptr Ds:[MAIN_VIEWPORT_STRUCT]
      Mov Ecx,Dword Ptr Ds:[Edx+0x398]
      Mov ModelModifier3,Ecx
      Mov Eax,MainTickCount
      Sub Eax,Dword Ptr Ss:[Ebp-0x74]
      Mov Dword Ptr Ss:[Ebp-0x68],Eax
      Mov Eax, FrameCount //-- fps
      Cmp Dword Ptr Ss:[Ebp-0x68],Eax
      Jge CONTINUE
      Mov Ecx, FrameCount
      Sub Ecx,Dword Ptr Ss:[Ebp-0x68]
      Mov Dword Ptr Ss:[Ebp-0x18C],Ecx
      Mov Edx,Dword Ptr Ss:[Ebp-0x18C]
      Mov SleepModifier,Edx
      NEXT:
      Push 1
      Call Dword Ptr Ds:[Sleep]
      Call Dword Ptr Ds:[GetTickCount]
      Sub Eax,Dword Ptr Ss:[Ebp-0x074]
      Cmp Eax,Dword Ptr Ss:[Ebp-0x18C]
      Jl NEXT
      Mov Eax,MainTickCount
      Cmp SyncTickCount,Eax
      Jnz HACK
      Mov Ecx,Dword Ptr Ss:[Ebp-0x6C]
      Cmp CountModifier,Ecx
      Jnz HACK
      Mov Edx,Dword Ptr Ss:[Ebp-0x74]
      Cmp DelayModifier,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ss:[Ebp-0x178]
      Cmp HasteModifier,Ecx
      Jnz HACK
      Mov Edx,Dword Ptr Ss:[Ebp-0x18C]
      Cmp SleepModifier,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ds:[0x07BC4F04]
      Mov Edx,Dword Ptr Ds:[Ecx+0x214]
      Cmp SpeedModifier1,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ds:[0x07BC4F04]
      Mov Edx,Dword Ptr Ds:[Ecx+0x218]
      Cmp SpeedModifier2,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ds:[0x07BC4F04]
      Mov Edx,Dword Ptr Ds:[Ecx+0x31A]
      Cmp ModelModifier1,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ds:[0x07BC4F04]
      Mov Edx,Dword Ptr Ds:[Ecx+0x394]
      Cmp ModelModifier2,Edx
      Jnz HACK
      Mov Ecx,Dword Ptr Ds:[0x07BC4F04]
      Mov Edx,Dword Ptr Ds:[Ecx+0x398]
      Cmp ModelModifier3,Edx
      Jnz HACK
      Add Eax,Dword Ptr Ss:[Ebp-0x18C]
      Mov MainTickCount,Eax
      Mov Eax, FrameCount //-- fps
      Mov Dword Ptr Ss:[Ebp-0x68],Eax
      CONTINUE:
      Mov Ecx,Dword Ptr Ss:[Ebp-0x178]
      Add Ecx,Dword Ptr Ss:[Ebp-0x68]
      Mov Dword Ptr Ss:[Ebp-0x6C],Ecx
      Jmp [CheckTickCountAddress1]
      HACK:
      Push 0
      Call Dword Ptr Ds:[ExitProcess]
   }
}



gProtect.cpp



int CProtect::FT_Nine2D()
{
   int FrameLimit = this->m_MainInfo.LimitFPS;

   switch(FrameLimit)
   {
   case 0:
      FrameLimit = 0x28; //-- Default: (25FPS)
      break;
   case 1:
      FrameLimit = 0x22; //-- Max value: (30FPS)
      break;
   case 2:
      FrameLimit = 0x1A; //-- Max value: (40FPS)
      break;
   case 3:
      FrameLimit = 0x14; //-- Max value: (50FPS)
      break;
   case 4:
      FrameLimit = 0x11; //-- Max value: (60FPS)
      break;
   default:
      FrameLimit = 0x5; //-- Max value: (Unlimit)
      break;
   }

   return FrameLimit;
}


under : SetWord(0x00E609E4,(gProtect->m_MainInfo.IpAddressPort)); // IpAddressPort



SetDword(0x004D597B, (DWORD)&MainTickCount);

SetDword(0x004DA289, (DWORD)&MainTickCount);

SetDword(0x004DA297, (DWORD)&MainTickCount);

SetDword(0x004DA3A2, (DWORD)&MainTickCount);

SetDword(0x004DA3CE, (DWORD)&MainTickCount);

SetDword(0x004DA3D9, (DWORD)&MainTickCount);

SetDword(0x0063D326, (DWORD)&MainTickCount);

SetDword(0x00642112, (DWORD)&MainTickCount);

SetCompleteHook(0xE9, 0x004DA280, &CheckTickCount1);

SetCompleteHook(0xE9, 0x004DA3A1, &CheckTickCount2);

VirtualizeOffset(0x004D9D39, 12);

VirtualizeOffset(0x004D9D45, 7);

VirtualizeOffset(0x004D9EFC, 15);

VirtualizeOffset(0x004DAC5C, 8);

VirtualizeOffset(0x005451F7, 5);

VirtualizeOffset(0x00545230, 8);

VirtualizeOffset(0x005A52E9, 8);

 

int RenderNotices()
{
    int MoveY = 80;

    int result; // eax@1
    if ( pPlayerState == 5 )
    {
        result = (unsigned __int8)pCheckWindow((int *)GetInstance(), 65);
        if ( (unsigned __int8)result != 1 )
        {
            EnableAlphaTest(1);
            pSetTextSize(pTextThis(), (HFONT)pFontBold);
            glColor3f(1.0, 1.0, 1.0);
            for (signed int i = 0; i < 6; ++i ){
                if ( *((BYTE *)0x813DDD0 + 264 * i + 260) )
                {
                    pSetTextColor(pTextThis(), 0x64u, 0xFFu, 0xC8u, 0xFFu);
                    pSetBackgroundTextColor(pTextThis(), 0, 0, 0, 0x80u);
                }
                else
                {
                    pSetBackgroundTextColor(pTextThis(), 0, 0, 0, 0x80u);
                    if ( NoticeInverse % 10 >= 5 )
                    {
                        pSetTextColor(pTextThis(), 0xFFu, 0xC8u, 0x50u, 0xFFu);
                    }
                    else
                    {
                        pSetTextColor(pTextThis(), 0xFFu, 0xC8u, 0x50u, 0x80u);
                    }
                }
                pDrawText(pTextThis(), 320, 3 * i + MoveY, (LPCTSTR)0x813DDD0 + 264 * i, 0, 0, (LPINT)8, 0);
            }
            result = NoticeInverse + 1;
        }
        return result;
    }
}

SetCompleteHook(0xE8, 0x004D5EC3, &RenderNotices);

 

Conclusión

Este código es fundamental para el control de la física y el movimiento en MU Online, asegurando que las animaciones y el comportamiento de los personajes sean fluidos y controlados. Además, incluye medidas de seguridad para prevenir trampas, lo que es crucial en un entorno competitivo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *