123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Rollback.mli *)typestats={stalls:int;rollbacks:int;replayed:int;deepest:int;dropped:int}(* a tick played but not confirmed: the model before it, and the
* inputs it was played with *)type'modelplayed={at:int;before:'model;used:stringarray}type'modelt={inputs:Inputs.t;delay:int;max_ahead:int;update:stringarray->'model->'model;on_confirm:int->'model->unit;mutablemodel:'model;(* the ticks from [confirmed] to [tick] - 1, oldest first *)mutablesaved:'modelplayedlist;mutabletick:int;mutableconfirmed:int;mutablestats:stats;}letcreate~(me:int)~(players:int)?(delay=0)?(max_ahead=8)~update~on_confirm(model:'model):'modelt={inputs=Inputs.create~me~players~delay;delay;max_ahead;update;on_confirm;model;saved=[];tick=0;confirmed=0;stats={stalls=0;rollbacks=0;replayed=0;deepest=0;dropped=0};}(*****************************************************************************)(* Guessing *)(*****************************************************************************)(* [player]'s input for [tick]: the real one if known, else the guess --
* the last one known before it (no key held if none) *)letbest(t:'modelt)(tick:int)(player:int):string=matchInputs.findt.inputs~tickplayerwith|Someinput->input|None->(letlast=min(Inputs.known_uptot.inputsplayer)(tick-1)inmatchiflast>=0thenInputs.findt.inputs~tick:lastplayerelseNonewithSomeinput->input|None->"")letinputs_for(t:'modelt)(tick:int):stringarray=Array.init(Inputs.playerst.inputs)(bestttick)(* play [tick] from [model], saved for a later rollback *)letplay(t:'modelt)(tick:int)(model:'model):'modelplayed*'model=letused=inputs_forttickin({at=tick;before=model;used},t.updateusedmodel)(*****************************************************************************)(* Fixing *)(*****************************************************************************)(* a saved tick one of whose guesses a real input now contradicts *)letwrong(t:'modelt)(p:'modelplayed):bool=letwrong_forplayerguess=matchInputs.findt.inputs~tick:p.atplayerwithSomereal->real<>guess|None->falseinArray.existsFun.id(Array.mapiwrong_forp.used)(* back to the earliest wrong tick, and every tick since played again *)letroll_back(t:'modelt):unit=letrecsplitkept=function|[]->None|p::rest->ifwrongtpthenSome(List.revkept,p)elsesplit(p::kept)restinmatchsplit[]t.savedwith|None->()|Some(kept,first)->letrecreplaytickmodelacc=iftick=t.tickthen(List.revacc,model)elseletp,model'=playttickmodelinreplay(tick+1)model'(p::acc)inletredone,model=replayfirst.atfirst.before[]inletn=List.lengthredoneint.saved<-kept@redone;t.model<-model;t.stats<-{t.statswithrollbacks=t.stats.rollbacks+1;replayed=t.stats.replayed+n;deepest=maxt.stats.deepestn}(* the oldest ticks whose inputs are all real: final, handed over, and
* no longer needed for a rollback *)letconfirm(t:'modelt):unit=letall_realtick=List.for_all(funp->Inputs.known_uptot.inputsp>=tick)(List.init(Inputs.playerst.inputs)Fun.id)inletrecgo()=matcht.savedwith|p::restwhenall_realp.at->(* the model after p: the next one's before, or the latest *)letafter=matchrestwithq::_->q.before|[]->t.modelint.on_confirmp.atafter;t.saved<-rest;t.confirmed<-p.at+1;go()|_->()ingo()letsettle(t:'modelt):unit=roll_backt;confirmtletstep(t:'modelt)(input:string):unit=settlet;ift.tick-t.confirmed>=t.max_aheadthent.stats<-{t.statswithstalls=t.stats.stalls+1}elsebeginInputs.add_minet.inputs~tick:(t.tick+t.delay)input;letp,model=playtt.tickt.modelint.saved<-t.saved@[p];t.model<-model;t.tick<-t.tick+1;confirmtendletmodel(t:'modelt):'model=t.modellettick(t:'modelt):int=t.tickletconfirmed(t:'modelt):int=t.confirmedletpacket(t:'modelt):string=Inputs.packett.inputsletreceive(t:'modelt)(bytes:string):unit=Inputs.receivet.inputsbytesletchecksum(t:'modelt)~(tick:int)(sum:int32):unit=Inputs.checksumt.inputs~ticksumletdesync(t:'modelt):(int*int)option=Inputs.desynct.inputsletstats(t:'modelt):stats={t.statswithdropped=Inputs.droppedt.inputs}