Page 1 of 1

Custom UDP packet composer/HID data/ Custom binary data parser

Posted: Sun Dec 07, 2025 11:55 pm
by miso
I do not know where to put this, but I think it's game related. This is a packet composer module for custom binary UDP packets for low payload, Can set/read individual bits, so I use the same for Hid data bitflags (buttons/hatswitches)

-theres no builtin boundary check, thats up to the user to keep in mind.
-allocates memory with noclear

I'm not sure if anyone will have a good use of it, but here's the module:

Code: Select all

DeclareModule NET
  EnableExplicit
  #PB_MAX_UDP     = 2048
  
  ;-HEADER BYTE FLAGS
  #_KEEPALIVE     = %00000001
  #_REQUEST       = %00000010
  #_ACK           = %00000100
  #_NACK          = %00001000
  #_PING          = %00010000
  #_PONG          = %00100000
  #_GM            = %01000000  ;game manager or game master flag
  #_ENCRYPTED     = %10000000
  
  ;-Packet Structure
  Structure pstruct
    *address
    size.i               ;size for received packets, ofset when composing a new packet
    sourceconnection.i   ;filled if received,0 if composed
    sourceip.i           ;filled if received,0 if composed
    sourceport.i         ;filled if received,0 if composed
    timestamp.q          ;filled when received or sent
    timeout.q            ;resend time if no response
    resent.i             ;counter, how many times it has been resent, or processed without ack/nack
    error.i              ;0 if no error
  EndStructure
  
  ;-Global variables  
  Global.i mtu         = 1024
  Global.i serverport  = 5000   ;must be open, portforwarded in your NAT for internet
  Global.u appid       = $5000 ;optionally, this magic number can be used as an endianness check    
  Global.i keepalive   = 15000 ;Client sends a keepalive packet if sending is absent for keepalive millisecs. Client Nat slot will be alive then.
                               ;RFC 4787 says, that an UDP mapping should not expire in less than 2 mins, but thats not garanteed.
  
  ;-Procedure Public declarations, Call these ONLY before starting a server or "joining" with client
  Declare.i SetMtu(newMTU.i)
  Declare.i SetServerPort(newport.i)
  Declare.i SetAppId(newappid.u)
  Declare.i SetClientKeepAlive(newtime.i)
  
  Declare.i newpacket(*packet.pstruct)
  Declare.i AddSByte(*packet.pstruct,byte.b)
  Declare.i AddUByte(*packet.pstruct,byte.a)
  Declare.i AddWord(*packet.pstruct,word.w)
  Declare.i AddUint16(*packet.pstruct,uint.u)
  Declare.i AddLong(*packet.pstruct,long.l)
  Declare.i AddQuad(*packet.pstruct,quad.q)
  Declare.i AddFloat(*packet.pstruct,float.f)
  Declare.i AddDouble(*packet.pstruct,double.d)
  Declare.i AddStringAscii(*packet.pstruct,string.s)
  Declare.i AddStringUTF8(*packet.pstruct,string.s)
  Declare.i AddStringUnicode(*packet.pstruct,string.s)
  Declare.i StartFlags(*packet.pstruct)
  Declare.i AddFlag(*packet.pstruct,bitposition)
  Declare.i ClearFlag(*packet.pstruct,bitposition)
  Declare.i SwitchFlag(*packet.pstruct,bitposition)
  Declare.i NextPosition(*packet.pstruct)
  Declare.i SetPosition(*packet.pstruct,position.i)
  Declare.i GetFlag(*packet.pstruct,bitposition.a)
  Declare.i FinishFlags(*packet.pstruct)
  Declare.b GetSByte(*packet.pstruct)
  Declare.a GetUByte(*packet.pstruct)
  Declare.w GetWord(*packet.pstruct)
  Declare.u GetUint16(*packet.pstruct)
  Declare.l GetLong(*packet.pstruct)
  Declare.q GetQuad(*packet.pstruct)
  Declare.f GetFloat(*packet.pstruct)
  Declare.d GetDouble(*packet.pstruct)
  Declare.s GetStringAscii(*packet.pstruct)
  Declare.s GetStringUTF8(*packet.pstruct)
  Declare.s GetStringUnicode(*packet.pstruct)
EndDeclareModule

Module NET
  Procedure.i SetMtu(newMTU.i)
    net::mtu = newMTU
    ProcedureReturn #True  
  EndProcedure
  
  ;sets the server port, call before opening a socket. If not called, default is 5000
  Procedure.i SetServerPort(newport.i)
    net::serverport = newport
    ProcedureReturn #True  
  EndProcedure
  
  Procedure.i SetAppId(newappid.u)
    net::appid = newappid
    ProcedureReturn #True  
  EndProcedure
  
  Procedure.i SetClientKeepAlive(newtime.i)
    net::keepalive = newtime
  EndProcedure
  
  ;PACKET COMPOSITION
  
  Procedure ResetPacket(*packet.pstruct)
    If *packet\address : FreeMemory(*packet\address) : EndIf
    *packet\address          = 0
    *packet\size             = 0
    *packet\sourceconnection = 0
    *packet\sourceip         = 0
    *packet\sourceport       = 0
    *packet\timestamp        = 0
    *packet\timeout          = 0
    *packet\resent           = 0
    *packet\error            = 0
  EndProcedure
  
  
  Procedure NewPacket(*packet.pstruct)
    ResetPacket(*packet)
    *packet\address = AllocateMemory(net::mtu,#PB_Memory_NoClear)
  EndProcedure
  
  ;*****************************************************************************************************
  ;Clears the current byte. It's needed before setting bitflags, as the memory is not cleared when allocated
  ;*****************************************************************************************************
  Procedure StartFlags(*packet.pstruct)
    PokeA((*packet\address+*packet\size),0)
  EndProcedure

  Procedure FinishFlags(*packet.pstruct)
    NextPosition(*packet)
  EndProcedure
  
  ;*****************************************************************************************************
  ;sets a bit to 1 at current memory position. Bitposition can be 0-7, where 0 is the least significant bit
  ;*****************************************************************************************************
  Procedure AddFlag(*packet.pstruct,bitposition)
    Protected original.a , flag.a
    If bitposition<0 Or bitposition>7 : ProcedureReturn #False : EndIf
    original.a = PeekA(*packet\address+*packet\size)
    flag.a = %1<<bitposition
    PokeA((*packet\address+*packet\size),original|flag)
  EndProcedure
  
  Procedure.i GetFlag(*packet.pstruct,bitposition.a)
    Protected original.a , mask.a
    If bitposition<0 Or bitposition>7 : ProcedureReturn #False : EndIf
    original.a = PeekA(*packet\address+*packet\size)
    mask.a = %1<<bitposition
    If mask & original : ProcedureReturn 1 : EndIf : ProcedureReturn 0
  EndProcedure
  
  ;*****************************************************************************************************
  ;sets a bit to 0 at current memory position. Bitposition can be 0-7, where 0 is the least significant bit
  ;*****************************************************************************************************
  Procedure ClearFlag(*packet.pstruct,bitposition)
    Protected original.a , flag.a
    If bitposition<0 Or bitposition>7 : ProcedureReturn #False : EndIf
    original.a = PeekA(*packet\address+*packet\size)
    flag.a = %1<<bitposition
    PokeA((*packet\address+*packet\size),original & (~flag))
  EndProcedure
  
  ;*****************************************************************************************************
  ;Inverts a bit at current memory position. Bitposition can be 0-7, where 0 is the least significant bit
  ;*****************************************************************************************************
  Procedure SwitchFlag(*packet.pstruct,bitposition)
    Protected original.a , flag.a
    If bitposition<0 Or bitposition>7 : ProcedureReturn #False : EndIf
    original.a = PeekA(*packet\address+*packet\size)
    flag.a = %1<<bitposition
    PokeA(*packet\address+*packet\size,original!flag)
  EndProcedure
  
  ;*****************************************************************************************************
  ;Steps forward one byteposition. This commands exist to step forward a position after flags had been set.
  ;*****************************************************************************************************
  Procedure NextPosition(*packet.pstruct)
    *packet\size + 1
  EndProcedure
  
  ;*****************************************************************************************************
  ;Sets the byteposition, position can be 0 - (packetsize-1 or MTU-1) Use this only if you know what you
  ;are doing With it. The final value of size will be the packet size when sending.
  ;This commands exist, because the parser can be used to parse any kind of binary data besides of packets, 
  ;like HID descriptor or read HID data
  ;*****************************************************************************************************
  Procedure SetPosition(*packet.pstruct,position.i)
    *packet\size = position
  EndProcedure
  
  ;*****************************************************************************************************
  ;Signed 1 byte integer
  ;*****************************************************************************************************
  Procedure AddSByte(*packet.pstruct,byte.b)
    PokeB(*packet\address+*packet\size,byte.b)
    *packet\size + 1
  EndProcedure
  
  ;*****************************************************************************************************
  ;Signed 1 byte integer
  ;*****************************************************************************************************
  Procedure.b GetSByte(*packet.pstruct)
    Protected retval.b
    retval.b = PeekB(*packet\address+*packet\size)
    *packet\size + 1
    ProcedureReturn retval.b
  EndProcedure
  
  ;*****************************************************************************************************
  ;Usigned 1 byte integer
  ;*****************************************************************************************************
  Procedure AddUByte(*packet.pstruct,byte.a)
    PokeA(*packet\address+*packet\size,byte.a)
    *packet\size + 1
  EndProcedure
  
  ;*****************************************************************************************************
  ;Unsigned 1 byte integer
  ;*****************************************************************************************************
  Procedure.a GetUByte(*packet.pstruct)
    Protected retval.a
    retval.a = PeekB(*packet\address+*packet\size)
    *packet\size + 1
    ProcedureReturn retval.a
  EndProcedure

  
  ;*****************************************************************************************************
  ;signed 2 byte integer
  ;*****************************************************************************************************
  Procedure AddWord(*packet.pstruct,word.w)
    PokeW(*packet\address+*packet\size,word.w)
    *packet\size + 2
  EndProcedure
  
  ;************************************************************
  ;Reads a word from the current position
  ;************************************************************
  Procedure.w GetWord(*packet.pstruct)
    Protected retval.w
    retval.w = PeekW(*packet\address+*packet\size)
    *packet\size + 2
    ProcedureReturn retval.w
  EndProcedure
  ;*****************************************************************************************************
  ;unsigned 2 byte integer
  ;*****************************************************************************************************
  Procedure AddUint16(*packet.pstruct,uint.u)
    PokeU(*packet\address+*packet\size,uint.u)
    *packet\size + 2
  EndProcedure
  
  ;************************************************************
  ;Reads an uint16 from the current position
  ;************************************************************
  Procedure.u GetUint16(*packet.pstruct)
    Protected retval.u
    retval.u = PeekU(*packet\address+*packet\size)
    *packet\size + 2
    ProcedureReturn retval.u
  EndProcedure
  ;*****************************************************************************************************
  ;signed 4 byte integer
  ;*****************************************************************************************************
  Procedure AddLong(*packet.pstruct,long.l)
    PokeL(*packet\address+*packet\size,long.l)
    *packet\size + 2
  EndProcedure
  
  ;************************************************************
  ;Reads a long from the current position
  ;************************************************************
  Procedure.l GetLong(*packet.pstruct)
    Protected retval.l
    retval.l = PeekB(*packet\address+*packet\size)
    *packet\size + 2
    ProcedureReturn retval.l
  EndProcedure
  ;*****************************************************************************************************
  ;signed 8 byte integer
  ;*****************************************************************************************************
  Procedure AddQuad(*packet.pstruct,quad.q)
    PokeQ(*packet\address+*packet\size,quad.q)
    *packet\size + 8
  EndProcedure
  
  ;************************************************************
  ;Reads a quad from the current position
  ;************************************************************
  Procedure.q GetQuad(*packet.pstruct)
    Protected retval.q
    retval.q = PeekQ(*packet\address+*packet\size)
    *packet\size + 8
    ProcedureReturn retval.q
  EndProcedure
  ;*****************************************************************************************************
  ;signed 4 byte float
  ;*****************************************************************************************************
  Procedure AddFloat(*packet.pstruct,float.f)
    PokeF(*packet\address+*packet\size,float.f)
    *packet\size + 4
  EndProcedure
  
  ;************************************************************
  ;Reads a float from the current position
  ;************************************************************
  Procedure.f GetFloat(*packet.pstruct)
    Protected retval.f
    retval.f = PeekF(*packet\address+*packet\size)
    *packet\size + 4
    ProcedureReturn retval.f
  EndProcedure
  
  ;*****************************************************************************************************
  ;signed 8 byte float (double)
  ;*****************************************************************************************************
  Procedure AddDouble(*packet.pstruct,double.d)
    PokeD(*packet\address+*packet\size,double.d)
    *packet\size + 8
  EndProcedure
  
  ;************************************************************
  ;Reads a double from the current position
  ;************************************************************
  Procedure.d GetDouble(*packet.pstruct)
    Protected retval.d
    retval.d = PeekD(*packet\address+*packet\size)
    *packet\size + 8
    ProcedureReturn retval.d
  EndProcedure
  ;*****************************************************************************************************
  ;adds an ascii zerostring to the packet
  ;*****************************************************************************************************
  Procedure AddStringAscii(*packet.pstruct,string.s)
    Protected i.i,r.s
    For i = 1 To Len(string.s)
      r.s=Mid(string,i,1)
      PokeA(*packet\address+*packet\size,Asc(r))
      *packet\size + 1
    Next i
    AddSByte(*packet,0)
  EndProcedure
  
  ;************************************************************
  ;Reads an Ascii string from the current position
  ;************************************************************
  Procedure.s GetStringAscii(*packet.pstruct)
    Protected i.i,r.s,_offset.u
    Repeat
      i = PeekA(*packet\address+*packet\size+_offset)
      r.s=r.s+Chr(i)
      _offset+1
    Until i = 0 Or _offset => net::mtu-1
    *packet\size +_offset
    ProcedureReturn r.s
  EndProcedure
  
  ;*****************************************************************************************************
  ;adds a UTF8 string to the packet in the format 2byte.u stringbytesize+ 2byte.u charactercount + string data
  ;*****************************************************************************************************
  Procedure AddStringUTF8(*packet.pstruct,string.s)
    Protected bytesize.u,length.u
    bytesize = StringByteLength(string.s,#PB_UTF8)
    length = Len(string)
    PokeU(*packet\address+*packet\size,bytesize)
    PokeU(*packet\address+*packet\size+2,length)
    PokeS(*packet\address+*packet\size+4,string,bytesize,#PB_UTF8)
    *packet\size=*packet\size+bytesize+4
  EndProcedure
  
  ;************************************************************
  ;Reads an UTF8 string from the current position
  ;************************************************************
  Procedure.s GetStringUTF8(*packet.pstruct)
    Protected bytesize.u,length.u,r.s
    bytesize.u = PeekU(*packet\address+*packet\size)
    length.u = PeekU(*packet\address+*packet\size+2)
    r.s = PeekS(*packet\address+*packet\size+4,length,#PB_UTF8)
    *packet\size=*packet\size+4+bytesize
    ProcedureReturn r.s
  EndProcedure
  
  ;************************************************************
  ;adds a Unicode string to the packet in the format 2byte .u stringbytesize+ 2byte charactercount + string data
  ;************************************************************
  Procedure AddStringUnicode(*packet.pstruct,string.s)
    Protected bytesize.u,length.u
    bytesize = StringByteLength(string.s,#PB_Unicode)
    length = Len(string.s)
    PokeU(*packet\address+*packet\size,bytesize)
    PokeU(*packet\address+*packet\size+2,length)
    PokeS(*packet\address+*packet\size+4,string,length,#PB_Unicode)
    *packet\size=*packet\size+bytesize+4
  EndProcedure
  
  ;************************************************************
  ;Reads a Unicode string from the current position
  ;************************************************************
  Procedure.s GetStringUnicode(*packet.pstruct)
    Protected length.u,r.s,bytesize.u
    bytesize.u = PeekU(*packet\address+*packet\size)
    length.u = PeekU(*packet\address+*packet\size+2)
    r.s = PeekS(*packet\address+*packet\size+4,length,#PB_Unicode)
    *packet\size=*packet\size+4+bytesize
    ProcedureReturn r.s
  EndProcedure

  
  ;************************************************************
  ;same as resetpacket with a different name. Don't ask, it will
  ;be more to this later.
  ;************************************************************
  Procedure DropPacket(*packet.pstruct)
    ResetPacket(*packet.pstruct)
  EndProcedure
EndModule

And heres an example composer (copy this after the module to make it work):

Code: Select all

;-=====Example======
DeclareModule SERVER
  EnableExplicit
  Global alive.a
  Global id.i
  Global sentpackets.i, receivedpackets.i
  Global composer.net::pstruct
    
  Global NewList sentpackets.net::pstruct()
  Global NewList receivedpackets.net::pstruct()
  
  Declare Startpublic()
  Declare StartLocal()
  
  Declare.i newpacket()
  Declare.i AddSByte(byte.b)
  Declare.i AddUByte(byte.a)
  Declare.i AddWord(word.w)
  Declare.i AddUint16(uint.u)
  Declare.i AddLong(long.l)
  Declare.i AddQuad(quad.q)
  Declare.i AddFloat(float.f)
  Declare.i AddDouble(double.d)
  Declare.i AddStringAscii(string.s)
  Declare.i AddStringUTF8(string.s)
  Declare.i AddStringUnicode(string.s)
  Declare.i AddFlag(bitposition)
  Declare.i ClearFlag(bitposition)
  Declare.i SwitchFlag(bitposition)
  Declare.i NextPosition()
  Declare.i SetPosition(position.i)
  Declare.i StartFlags()
  Declare.i FinishFlags()
  Declare.i GetFlag(bitposition.i)
  Declare.b GetSByte()
  Declare.a GetUByte()
  Declare.w GetWord()
  Declare.u GetUint16()
  Declare.l GetLong()
  Declare.q GetQuad()
  Declare.f GetFloat()
  Declare.d GetDouble()
  Declare.s GetStringAscii()
  Declare.s GetStringUTF8()
  Declare.s GetStringUnicode()
EndDeclareModule

Module SERVER
  
  Procedure.i StartPublic()
    If server::alive = #True : ProcedureReturn  #False : EndIf
    id = CreateNetworkServer(#PB_Any,net::serverport,#PB_Network_UDP|#PB_Network_IPv4)
    server::alive=#True
    ProcedureReturn #True  
  EndProcedure
  
  Procedure.i StartLocal()
    If server::alive = #True : ProcedureReturn  #False : EndIf
    id = CreateNetworkServer(#PB_Any,net::serverport,#PB_Network_UDP|#PB_Network_IPv4,"127.0.0.1")
    server::alive=#True
    ProcedureReturn #True  
  EndProcedure
  
   ;PACKET COMPOSITION
  Procedure NewPacket()
    net::newpacket(@composer)
  EndProcedure
  
  ;Signed 1 byte integer
  Procedure AddSByte(byte.b)
    net::AddSByte(@composer,byte.b)
  EndProcedure
  
  ;unsigned 1 byte integer
  Procedure AddUByte(byte.a)
    net::AddUByte(@composer,byte.a) 
  EndProcedure
  
  ;signed 2 byte integer
  Procedure AddWord(word.w)
    net::addWORD(@composer,word.w)
  EndProcedure
  
  ;unsigned 2 byte integer
  Procedure AddUint16(uint.u)
    net::AddUint16(@composer,uint.u)
  EndProcedure
  
  ;signed 4 byte integer
  Procedure AddLong(long.l)
    net::AddLong(@composer,long.l)
  EndProcedure
  
  ;signed 8 byte integer
  Procedure AddQuad(quad.q)
    net::AddQuad(@composer,quad.q)
  EndProcedure
  
  ;signed 4 byte float
  Procedure AddFloat(float.f)
    net::AddFloat(@composer,float.f)
  EndProcedure
  
  ;signed 8 byte float (double)
  Procedure AddDouble(double.d)
    net::AddDouble(@composer,double.d)
  EndProcedure
  
  ;adds an ascii zerostring to the packet
  Procedure AddStringAscii(string.s)
    net::AddStringAscii(@composer,string.s)
  EndProcedure
  
  ;adds an utf8 string to the packet in the format 2byte .u stringbytesize + string data
  Procedure AddStringUTF8(string.s)
    net::AddStringUTF8(@composer,string.s)
  EndProcedure
  
  ;adds a Unicode string to the packet in the format 2byte .u stringbytesize + string data
  Procedure AddStringUnicode(string.s)
    net::AddStringUnicode(@composer,string.s)
  EndProcedure
  
  Procedure StartFlags()
    net::startflags(@composer)
  EndProcedure

  Procedure FinishFlags()
    net::finishflags(@composer)
  EndProcedure
  
  Procedure AddFlag(bitposition)
    net::AddFlag(@composer,bitposition)
  EndProcedure
  
  Procedure ClearFlag(bitposition)
    net::ClearFlag(@composer,bitposition)
  EndProcedure
  
  Procedure SwitchFlag(bitposition)
    net::SwitchFlag(@composer,bitposition)
  EndProcedure
  
  Procedure Nextposition()
    net::NextPosition(@composer)
  EndProcedure
  
  Procedure Setposition(bitposition)
    net::Setposition(@composer,bitposition)
  EndProcedure
  
  Procedure.i GetFlag(bitposition.i)
    ProcedureReturn net::GetFlag(@composer,bitposition)
  EndProcedure
  
  Procedure.b GetSbyte()
    ProcedureReturn net::Getsbyte(@composer)
  EndProcedure
  
  Procedure.a GetUByte()
    ProcedureReturn net::GetuByte(@composer)
  EndProcedure

  Procedure.s GetStringUnicode()
    ProcedureReturn net::GetStringUnicode(@composer)
  EndProcedure
  
  Procedure.d GetDouble()
    ProcedureReturn net::GetDouble(@composer)
  EndProcedure

  Procedure.l GetLong()
    ProcedureReturn net::Getlong(@composer)
  EndProcedure
  
  Procedure.s GetStringUTF8()
    ProcedureReturn net::GetStringUTF8(@composer)
  EndProcedure

  Procedure.f GetFloat()
    ProcedureReturn net::Getfloat(@composer)
  EndProcedure
  
  Procedure.s GetStringAscii()
    ProcedureReturn net::GetStringAscii(@composer)
  EndProcedure
  
  Procedure.s GetWord()
    ProcedureReturn net::GetStringUnicode(@composer)
  EndProcedure
  
  Procedure.q GetQuad()
    ProcedureReturn net::GetQuad(@composer)
  EndProcedure
  
  Procedure.u GetUint16()
    ProcedureReturn net::GetUint16(@composer)
  EndProcedure

EndModule



;-=============Example, composing a packet
server::newpacket()     ;frees previously allocated memory automatically
server::AddUByte(244)
server::AddSByte(-100)
server::AddUint16(214)

server::startflags() ; clears all bits of the current byte, as the memory had been allocated with noclear.
server::AddFlag(0)
server::AddFlag(1)
server::AddFlag(7)
server::Finishflags() ; increase the offset position with 1, as flags do not increase it automatically

server::AddStringAscii("Hello World! (AscII)")
server::AddStringUTF8("Hello World! (UTF8)")
server::AddStringUnicode("Hello World! (Unicode)")
server::adddouble(3.14)
server::addfloat(3.14)

Debug "The composed packets Address is "+Str(server::composer\address)
Debug "and its size is : " + Str(server::composer\size) + " bytes long, ready to send."
;-=============Reading data back in order
Debug " "
Debug "Proofreading some data back:"
server::SetPosition(0)
Debug server::getUByte()
Debug server::getSByte()
Debug server::GetUint16()

Debug server::getflag(0)
Debug server::getflag(2)
server::NextPosition()

Debug server::GetStringascii()
Debug server::GetStringUTF8()
Debug server::GetStringUnicode()
Debug server::GetDouble()
Debug server::getfloat()
Debug "Read Packet size : " + Str(server::composer\size)
If you find a bug, please inform me too ;)

Re: Custom UDP packet composer/HID data/ Custom binary data parser

Posted: Mon Dec 08, 2025 1:33 pm
by minimy
Hello miso, this is fantastic! Really nice framework for UDP. Clever way to send massive data.
With this and the 360 optimization the packets are really small.
Thank you very much!

Re: Custom UDP packet composer/HID data/ Custom binary data parser

Posted: Mon Dec 08, 2025 2:26 pm
by miso
Thanks! ;) At least it made one person happy. For a client, it's enough to rename the server and add a connect procedure instead of startserver. I do know it won't cause a problem to fill the composer data with the parameters of an arrived packet to begin to parse. For string entry UTF8 and Unicode can be used without the bytelength, that would save another 2 bytes per string. ( I don't know why I did that in first place. )
It can be easily adjusted to run a host and a client on a different thread if needed. Have fun with it, happy coding;)