--Copyright Cheat Engine

--local 
UnityEngineTimeSetTimeScaleMethod=nil

function getOriginalCodeAndFiller(address, farjmp)
  local original,filler

  if type(address)~='number' then
    address=getAddressSafe(address)
  end

  if address==nil then
    return nil, 'invalid address'
  end

  local sl=createStringList()
  local d=createDisassembler()
  local size=0
  local jmpsize=not farjmp and 5 or 14
  
  
  while size<jmpsize do
    d.disassemble(address)
    local ldd=d.LastDisassembleData
    local inst=ldd.opcode..' '..ldd.parameters
    sl.add(inst)
    size=size+#ldd.bytes
    address=address+#ldd.bytes
  end

  original=sl.Text
  if size-jmpsize>0 then
    filler=string.format("nop %x", size-jmpsize)
  else
    filler=''
  end

  sl.destroy()
  d.destroy()
  return original,filler
end 

function hookSpeedFunctionsLinux()
  local result, data=autoAssemble([[

{$c linux}
//force linux as when you target an .EXE it switched to windows abi

#include <stdint.h>
#include <celog.h>

struct timespec {
  time_t  tv_sec;   /* Seconds */
  long    tv_nsec;  /* Nanoseconds */
};

struct timeval {
  long tv_sec;
  long tv_usec;
};

struct timezone {
  int tz_minuteswest;
  int tz_dsttime;
};

typedef int clockid_t;

typedef int (*GETTIMEOFDAY) (struct timeval *tv, struct timezone *tz);
typedef int (*CLOCK_GETTIME) (int clk_id, struct timespec *tp);

extern int clock_gettime(int clockid, struct timespec *tp);
extern int gettimeofday(struct timeval *tv, struct timezone *tz);
void speedhack_initialize(void);


extern int real_gettimeofday(struct timeval *tv, struct timezone *tz);
extern int real_clock_gettime(int clockid, struct timespec *tp);


#define PTHREAD_ONCE_INIT 0

float speedhack_wantedspeed=1.0f;
int initialized=0;

typedef int pthread_once_t ;

pthread_once_t speedhack_initonce=PTHREAD_ONCE_INIT;

typedef struct{
  char mutexstuff[128]; //this should be more than enough
} pthread_mutex_t;

pthread_mutex_t tod_cs;
pthread_mutex_t cgt_cs;


extern int pthread_mutex_lock(pthread_mutex_t *mutex);
extern int pthread_mutex_unlock(pthread_mutex_t *mutex);
extern int pthread_mutex_init(pthread_mutex_t *mutex, const void *attr);


extern double floor(double x);
extern int pthread_once (pthread_once_t *__once_control, void (*__init_routine) (void));

struct {
  int result;
  struct timespec initialoffset;
  struct timespec initialtime;

  pthread_mutex_t cs;
  float clockspeed;
} initialclock[10];

struct timeval initial_offset_tod_tv;
struct timeval initial_time_tod_tv;

int new_clock_gettime(clockid_t clk_id, struct timespec *tp)
{
  int r=-1;
  struct timespec currenttp;

 // debug_log("new_clock_gettime\n");

  if (!initialized)
    pthread_once(&speedhack_initonce, speedhack_initialize);

 // debug_log("after speedhack_initialize\n");

  r=real_clock_gettime(clk_id, &currenttp);

  if ((r==0) && (clk_id<=9) && (initialclock[clk_id].result==0))
  {
    struct timespec temptp;

    pthread_mutex_lock(&initialclock[clk_id].cs);

    temptp.tv_sec=currenttp.tv_sec-initialclock[clk_id].initialtime.tv_sec;
    temptp.tv_nsec=currenttp.tv_nsec-initialclock[clk_id].initialtime.tv_nsec;

    if (temptp.tv_nsec<0)
    {
      temptp.tv_nsec+=1000000000;
      temptp.tv_sec--;
    }

   // debug_log("delta : sec=%ld  nsec=%ld\n", temptp.tv_sec, temptp.tv_nsec);
    double multiplier=initialclock[clk_id].clockspeed;

    double newsec_double = (double)temptp.tv_sec*multiplier;

    int64_t newnsec=floor((double)temptp.tv_nsec*multiplier);
    int64_t newsec=floor(newsec_double);

    newnsec+=floor((newsec_double - floor(newsec_double)) * 1000000000.0f);


    //add the initial offset
    newsec+=initialclock[clk_id].initialoffset.tv_sec;
    newnsec+=initialclock[clk_id].initialoffset.tv_nsec;


    //sanitize the results (no negative usec, and add usec values of 1000000 to sec)
    newsec+=newnsec / 1000000000;  //if newsec is negative this will decrease it with the number of seconds it represents
    newnsec=newnsec % 1000000000;


    if (newnsec<0)
    {
      newnsec+=1000000000;
      newsec--;
    }

    if (tp)
    {
      tp->tv_sec=newsec;
      tp->tv_nsec=newnsec;

      //debug_log("clock_gettime %d: from %d.%d  ->  %d.%d\n", clk_id, currenttp.tv_sec, currenttp.tv_nsec, tp->tv_sec, tp->tv_nsec);

    }





    volatile int *oldspeed=(volatile int*)&initialclock[clk_id].clockspeed;
    volatile int *newspeed=(volatile int*)&speedhack_wantedspeed;

    if ((*oldspeed) != (*newspeed))
    {
     // debug_log("speed was changed\n");
      if (tp)
      {
        initialclock[clk_id].initialoffset=*tp;
        initialclock[clk_id].initialtime=currenttp;

        initialclock[clk_id].clockspeed=speedhack_wantedspeed;
      }
    }
    else
    {
     // debug_log("speed was not changed\n");
    }
    pthread_mutex_unlock(&initialclock[clk_id].cs);
  }
  else
  {

    if (r && tp)
    {
      //debug_log("unhandled time %d\n", clk_id);
      *tp=currenttp;
    }
  }

  if (r)
  {
  //debug_log("r was not 0\n");
  }

  return r;
}

float gettimeofday_speed=1.0f;
int new_gettimeofday(struct timeval *tv, struct timezone *tz)
{
  int r;

  struct timeval currenttv;
  struct timeval temptv;

  //debug_log("new_gettimeofday\n");

  if (!initialized)
  {
  //init
   // debug_log("speedhack was not initialized\n");
    pthread_once(&speedhack_initonce, speedhack_initialize);
  //  debug_log("speedhack is now initialized:%d\n", initialized);
  }

 // debug_log("after speedhack_initialize\n");


  //get the current real time
 // debug_log("calling real_gettimeofday\n");
  r=real_gettimeofday(&currenttv, tz);
 // debug_log("after calling real_gettimeofday r=%d\n",r);

  //Subtract the real time with the initial time(real) and multiply that with the speed and add it to the initial time offset (fake)

  //assume that currenttv is ALWAYS bigger than initial_time (or at least equal)

  pthread_mutex_lock(&tod_cs);

  temptv.tv_sec=currenttv.tv_sec-initial_time_tod_tv.tv_sec;
  temptv.tv_usec=currenttv.tv_usec-initial_time_tod_tv.tv_usec;

  if (temptv.tv_usec<0)
  {
    temptv.tv_usec+=1000000;
    temptv.tv_sec--;
  }


  //debug_log("delta : sec=%ld  usec=%ld\n", temptv.tv_sec, temptv.tv_usec);

  //temptv now contains the relative time passed since the speedhack got enabled/set a new speed

  //because of the high values multiply in two steps instead of one like on windows where 64-bit is the biggest time value
  //(and not all systems seem to support uint128)
  double multiplier=gettimeofday_speed;

  double newsec_double = (double)temptv.tv_sec*multiplier;

  int64_t newusec=floor((double)temptv.tv_usec*multiplier);
  int64_t newsec=floor(newsec_double);

  //convert the decimal part of sec to usec

  newusec+=floor((newsec_double - floor(newsec_double)) * 1000000.0f);



  //debug_log("newsec=%ld  newusec=%ld\n", newsec, newusec);

  //add the initial offset
  newsec+=initial_offset_tod_tv.tv_sec;
  newusec+=initial_offset_tod_tv.tv_usec;




  //sanitize the results (no negative usec, and add usec values of 1000000 to sec)
  newsec+=newusec / 1000000;  //if newsec is negative this will decrease it with the number of seconds it represents
  newusec=newusec % 1000000;


  if (newusec<0)
  {
    newusec+=1000000;
    newsec--;
  }



  if (tv)
  {
    tv->tv_sec=newsec;
    tv->tv_usec=newusec;
  }

  volatile int *oldspeed=(volatile int*)&gettimeofday_speed;
  volatile int *newspeed=(volatile int*)&speedhack_wantedspeed;

  if ((*oldspeed) != (*newspeed))
  {
    //debug_log("speed was changed\n");
    if (tv)
    {
      initial_offset_tod_tv=*tv;
      initial_time_tod_tv=currenttv;

      gettimeofday_speed=speedhack_wantedspeed;
    }
  }
  else
  {
    //debug_log("speed was not changed\n");
  }

  pthread_mutex_unlock(&tod_cs);

  return r;
}


void speedhack_initialize(void)
{
  int i;

  if (initialized)
    return;



  //debug_log("speedhack_initialize\n");
  i=pthread_mutex_init(&tod_cs, NULL);

  //debug_log("pthread_mutex_init returned %d (0 = success)\n",i);

  real_gettimeofday(&initial_time_tod_tv, NULL);
  initial_offset_tod_tv=initial_time_tod_tv;

  gettimeofday_speed=speedhack_wantedspeed;

  for (i=0; i<=9; i++)
  {
    real_clock_gettime(i, &initialclock[i].initialoffset);
    initialclock[i].result=real_clock_gettime(i, &initialclock[i].initialtime);

    pthread_mutex_init(&initialclock[i].cs, NULL);
    initialclock[i].clockspeed=speedhack_wantedspeed;
  }

  initialized=1;
  //debug_log("speedhack_initialize success\n");
}

/*
int testcode()
{
  struct timeval tv;
  struct timezone tz;
  int i;

  speedhack_initialize();

  debug_log("\nbla()\n");
  debug_log("calling new_gettimeofday()\n");

  struct timespec ts;
  debug_log("Calling new_clock_gettime 0\n");
  i=new_clock_gettime(0,&ts);

  debug_log("i=%d\n");
  debug_log("ts_sec=%d\n", ts.tv_sec);
  debug_log("ts_nsec=%d\n", ts.tv_nsec);

  debug_log("Calling new_clock_gettime 1\n");
  i=new_clock_gettime(1,&ts);

  debug_log("i=%d\n");
  debug_log("ts_sec=%d\n", ts.tv_sec);
  debug_log("ts_nsec=%d\n", ts.tv_nsec);





  return 1234;
}*/


{$asm}


hook(clock_gettime,new_clock_gettime,real_clock_gettime) //vdso takes priority
hook(gettimeofday,new_gettimeofday,real_gettimeofday)
]]) 

  return result, data

 
end

function hookSpeedFunctions()
  --print("hookSpeedFunctions")
  if getAddressSafe("speedhack_wantedspeed")~=nil then
  --  print("already hooked")
    return true
  end
  
  UnityEngineTimeSetTimeScaleMethod=nil
  
  local r,r2=injectCEHelperLib()
  
  if not r then
    messageDialog('error in injectCEHelperLib(): '..r2, mtError,mbOK)
    return false
  end
  
  local result, data=autoAssemble([[
    globalalloc(speedhack_wantedspeed,4)
    speedhack_wantedspeed:
    dd (float)1
    
    globalalloc(speedhack_unityspeedhackmethod,4)
    speedhack_unityspeedhackmethod:
    dd 1
{$asm}

  ]])

  if not result then
    messageDialog(data)
    return
  end

 -- print("allocated speedhack_wantedspeed")
 
  if monoSettings.Value["SkipMonoSpeedhack"]~='1' then
    local hasMono=getAddressSafe('mono_thread_attach',false,true) or getAddressSafe('il2cpp_thread_attach',false,true)

    if hasMono then
      LaunchMonoDataCollector()
      
      local r=mono_findClass("UnityEngine","Time")
      if r then

        UnityEngineTimeSetTimeScaleMethod=mono_class_findMethod(r,"set_timeScale")
        if UnityEngineTimeSetTimeScaleMethod then    

          local set_timeScale
 
          
          if mono_isil2cpp() then
            set_timeScale=mono_compile_method(UnityEngineTimeSetTimeScaleMethod)
          else          
            set_timeScale=getAddressSafe("UnityEngine.Time:set_timeScale")
          end 
          
          if set_timeScale then   
            local originalcode,filler=getOriginalCodeAndFiller(set_timeScale, true)
          
            if originalcode then            
              local s=string.format([[
{$c}  
extern int speedhack_unityspeedhackmethod; 
extern float speedhack_wantedspeed;
float getNewSpeed(float newspeed)
{
  float wanted=speedhack_wantedspeed; //bug in tcc needs local first
  int method=speedhack_unityspeedhackmethod;
  
  if (method==0) //method 0 : If speed==0, return 0, else return the speedhack speed
  {
    if (newspeed>0)
      newspeed=wanted;    
  }
  else //method 1: multiply the wanted speed by the speedhack speed. 0*wanted==0, 1*wanted=speedhack speed, 2*wanted=double speedhack speed.  Only issue is if it where to read the old speed and apply it's own multiplication
    newspeed=newspeed*wanted;

  return newspeed;
}
{$asm}
alloc(set_timeScaleEntryHook,128)
registersymbol(set_timeScaleEntryHook)
label(returnhere)
set_timeScaleEntryHook:
sub rsp,20
call getNewSpeed //will change xmm0
add rsp,20


//previous version's implementation
 // mov rax,speedhack_wantedspeed 
//  movss xmm0,[rax]

%s
jmp returnhere

%x:
jmp far set_timeScaleEntryHook
%s

returnhere:]], originalcode, set_timeScale, filler) 
              local r,err=autoAssemble(s)
              if r then return true end --just this should be enough
 
              --print(s)
              
              return true
            end
          end
        else
          print("no UnityEngineTimeSetTimeScaleMethod")
        end
      end
    end
  end
  
    

  local gtcaddress=getAddressSafe('kernel32.gettickcount64')
  if gtcaddress==nil then
    waitforExports()
    gtcaddress=getAddressSafe('kernel32.gettickcount64')

    if (gtcaddress==nil) then
      reinitializeSymbolhandler()
      gtcaddress=getAddressSafe('kernel32.gettickcount64')
      if (gtcaddress==nil) then
        messageDialog('Failure finding kernel32.gettickcount64', mtError, mbOK)
        return false
      end
    end
  end


  local originalcode,filler=getOriginalCodeAndFiller(gtcaddress)

  if originalcode then
    local s=string.format([[

alloc(gtc_originalcode,64,"kernel32.gettickcount64")
label(gtc_returnhere)
label(gtchook_exit)

{$c}

#include <stdint.h>
#include <stddef.h>
#include <celib.h>
#include <windowslite.h>

__stdcall uint64_t gtc_originalcode(void);
float gtc_speed=1.0f;
uint64_t gtc_initialtime=0;
uint64_t gtc_initialoffset=0;
CRITICAL_SECTION gtc_cs;

void *gtc_initonce=(void*)0;
int gtc_intialized=0;


extern float speedhack_wantedspeed;

__stdcall int InitOnceExecuteOnce(void *InitOnce, void* InitFn, void* Parameter, void *Context);


__stdcall int initgtc_cs(void *InitOnce, void *Parameter, void *lpContext) {
  InitializeCriticalSection(&gtc_cs);
  gtc_intialized=1;
  return TRUE;
}



__stdcall uint64_t new_gettickcount(void)
{
  uint64_t newtime;

  uint64_t currenttime;
  float wantedspeed; //small issue with tcc where you can not compare against extern directly


  currenttime=gtc_originalcode();

  if (gtc_intialized==0) //only set to true once the cs has been initialized
    InitOnceExecuteOnce(&gtc_initonce, initgtc_cs, NULL, NULL);

  //after here, gtc_cs is initialized
  EnterCriticalSection(&gtc_cs);




  //csenter(&gtc_cs);
  wantedspeed=speedhack_wantedspeed;

  if (gtc_initialtime==0)
  {
    gtc_initialtime=currenttime;
    gtc_initialoffset=currenttime;
  }

  newtime=(currenttime-gtc_initialtime)*gtc_speed;
  newtime=newtime+gtc_initialoffset; //don't put in in the calculation above, as it gets converted to float, and truncated

  if (gtc_speed!=wantedspeed)
  {
    //the user wants to change the speed
    gtc_initialoffset=newtime;
    gtc_initialtime=currenttime;
    gtc_speed=speedhack_wantedspeed;
  }



  LeaveCriticalSection(&gtc_cs);


  return newtime;

}
{$asm}


gtc_originalcode:
%s

gtchook_exit:
jmp gtc_returnhere

kernel32.gettickcount64:
jmp new_gettickcount
%s

gtc_returnhere:

{$ifdef kernel32.timegettime}
kernel32.timeGetTime:
jmp new_gettickcount
{$endif}

kernel32.getTickCount:
jmp new_gettickcount

]],originalcode, filler)

    local result, data=autoAssemble(s) 

    if not result then
      if data==nil then
        data=' (no reason)'
      end
      messageDialog('Failure hooking kernel32.gettickcount64:'..data, mtError, mbOK)
    end
  end;


--queryPerformanceCounter
  local qpcaddress=getAddressSafe('ntdll.RtlQueryPerformanceCounter')
  if qpcaddress==nil then
    waitforExports()
    qpcaddress=getAddressSafe('ntdll.RtlQueryPerformanceCounter')

    if (qpcaddress==nil) then
      reinitializeSymbolhandler()
      qpcaddress=getAddressSafe('ntdll.RtlQueryPerformanceCounter')
      if (qpcaddress==nil) then
        messageDialog('Failure finding kernel32.gettickcount64', mtError, mbOK)
        return false
      end
    end
  end


  local originalcode,filler=getOriginalCodeAndFiller(qpcaddress)

  if originalcode then


  --speedhack does not disable. Just sets speed to 1 when done

    local s=string.format([[

alloc(qpc_originalcode,64,"ntdll.RtlQueryPerformanceCounter")
label(qpc_returnhere)
label(qpchook_exit)

{$c}
#include <stdint.h>
#include <stddef.h>
#include <celib.h>
#include <windowslite.h>


__stdcall int  qpc_originalcode(uint64_t *count);
float qpc_speed=1.0f;
uint64_t qpc_initialtime=0;
uint64_t qpc_initialoffset=0;
CRITICAL_SECTION qpc_cs;

void *qpc_initonce=(void*)0;
int qpc_intialized=0;


uint64_t qpc_lastresult=0;

extern float speedhack_wantedspeed;

__stdcall int InitOnceExecuteOnce(void *InitOnce, void* InitFn, void* Parameter, void *Context);


__stdcall int initqpc_cs(void *InitOnce, void *Parameter, void *lpContext) {
  InitializeCriticalSection(&qpc_cs);
  qpc_intialized=1;
  return TRUE;
}





__stdcall int  new_RtlQueryPerformanceCounter(uint64_t *count)
{
  uint64_t newtime;

  uint64_t currenttime;
  uint64_t newwantedspeed;


  float wantedspeed; //small issue with tcc where you can not compare against extern directly

  if (qpc_intialized==0) //only set to true once the cs has been initialized
    InitOnceExecuteOnce(&qpc_initonce, initqpc_cs, NULL, NULL);

  //after here, gtc_cs is initialized
  EnterCriticalSection(&qpc_cs);


  int result=qpc_originalcode(&currenttime);


  
  
  wantedspeed=speedhack_wantedspeed;

  if (qpc_initialtime==0)
  {
    qpc_initialtime=currenttime;
    qpc_initialoffset=currenttime;
  }

  newtime=(currenttime-qpc_initialtime)*qpc_speed;

  newtime=newtime+qpc_initialoffset;
  if (qpc_speed!=wantedspeed)
  {
    //the user wants to change the speed
    qpc_initialoffset=newtime;
    qpc_initialtime=currenttime;
    qpc_speed=speedhack_wantedspeed;
  }
  
  LeaveCriticalSection(&qpc_cs);

  

  *count=newtime;

  return result;

}
{$asm}


qpc_originalcode:
%s

qpchook_exit:
jmp qpc_returnhere

ntdll.RtlQueryPerformanceCounter:
jmp new_RtlQueryPerformanceCounter
%s

qpc_returnhere:


]],originalcode, filler)

    local result2, data2=autoAssemble(s)
    
    if not result2 then
      if data2==nil then
        data2=' (no reason)'
      end
      messageDialog('Failure hooking ntdll.RtlQueryPerformanceCounter:'..data2, mtError, mbOK)
    end
    
  end;

  return result or result2
end

function initSpeedHackHooks()
  local result, errormsg
  if isConnectedToCEServer() or (getOperatingSystem()==2) then --ceserver or linux version
    --linux
    if getAddressSafe("speedhack_wantedspeed")==nil then
      --still needs hooking
      UnityEngineTimeSetTimeScaleMethod=nil
      result,errormsg=hookSpeedFunctionsLinux()
    else
      result=true
    end
        
    return true, result, errormsg    
  else  
    if targetIsX86() then
      if getAddressSafe("speedhack_wantedspeed")==nil then
        --still needs hooking
        result,errormsg=hookSpeedFunctions()
      else
        result=true
      end
          
      return true, result, errormsg
    else
      return false
    end
  end
end


registerSpeedhackCallbacks(function() --OnActivate
  return initSpeedHackHooks()
end,

function(speed) --OnSetSpeed(speed)
  if initSpeedHackHooks() then
    writeFloat("speedhack_wantedspeed", speed)
    
    if UnityEngineTimeSetTimeScaleMethod then
      mono_invoke_method(nil,UnityEngineTimeSetTimeScaleMethod,nil,{speed}) 
    end
    
    result=true      
    
    return true, true
  else
    return false
  end
end)


