From 1860655ab07b3692c761d676657169e276dbf0c9 Mon Sep 17 00:00:00 2001 From: ShiKhu Date: Fri, 22 Jul 2022 13:06:52 +0200 Subject: [PATCH] feat(mobs): opensource message enc/dec generator --- mobs/README.md | 16 + mobs/ios_messages.rb | 172 ++++++++ mobs/messages.rb | 409 ++++++++++++++++++ mobs/primitives/primitives.go | 124 ++++++ mobs/primitives/primitives.py | 62 +++ mobs/primitives/primitives.swift | 60 +++ mobs/run.rb | 136 ++++++ .../backend~pkg~messages~filters.go.erb | 10 + .../backend~pkg~messages~get-timestamp.go.erb | 12 + .../backend~pkg~messages~messages.go.erb | 22 + .../backend~pkg~messages~read-message.go.erb | 26 ++ ...stributor~messages~RawMessageReader.ts.erb | 35 ++ ...MessageDistributor~messages~message.ts.erb | 13 + ...yer~MessageDistributor~messages~raw.ts.erb | 14 + mobs/templates/ios/ASMessage.swift | 36 ++ ...tracker~tracker~src~common~messages.ts.erb | 31 ++ 16 files changed, 1178 insertions(+) create mode 100644 mobs/README.md create mode 100644 mobs/ios_messages.rb create mode 100644 mobs/messages.rb create mode 100644 mobs/primitives/primitives.go create mode 100644 mobs/primitives/primitives.py create mode 100644 mobs/primitives/primitives.swift create mode 100644 mobs/run.rb create mode 100644 mobs/templates/backend~pkg~messages~filters.go.erb create mode 100644 mobs/templates/backend~pkg~messages~get-timestamp.go.erb create mode 100644 mobs/templates/backend~pkg~messages~messages.go.erb create mode 100644 mobs/templates/backend~pkg~messages~read-message.go.erb create mode 100644 mobs/templates/frontend~app~player~MessageDistributor~messages~RawMessageReader.ts.erb create mode 100644 mobs/templates/frontend~app~player~MessageDistributor~messages~message.ts.erb create mode 100644 mobs/templates/frontend~app~player~MessageDistributor~messages~raw.ts.erb create mode 100644 mobs/templates/ios/ASMessage.swift create mode 100644 mobs/templates/tracker~tracker~src~common~messages.ts.erb diff --git a/mobs/README.md b/mobs/README.md new file mode 100644 index 000000000..beee0030c --- /dev/null +++ b/mobs/README.md @@ -0,0 +1,16 @@ +# Message Object Binary Schema and Code Generator from Templates + + +To generate all necessary files for the project: + +```sh +ruby run.rb + +``` + +In order generated .go file to fit the go formatting style: +```sh +gofmt -w ../backend/pkg/messages/messages.go + +``` +(Otherwise there will be changes in stage) diff --git a/mobs/ios_messages.rb b/mobs/ios_messages.rb new file mode 100644 index 000000000..57e737714 --- /dev/null +++ b/mobs/ios_messages.rb @@ -0,0 +1,172 @@ +message 107, 'IOSBatchMeta', :replayer => false do + uint 'Timestamp' + uint 'Length' + uint 'FirstIndex' +end + +message 90, 'IOSSessionStart', :replayer => true do + uint 'Timestamp' + # uint 'Length' + + uint 'ProjectID' + string 'TrackerVersion' + string 'RevID' + string 'UserUUID' + # string 'UserAgent' + string 'UserOS' + string 'UserOSVersion' + # string 'UserBrowser' + # string 'UserBrowserVersion' + string 'UserDevice' + string 'UserDeviceType' + # uint 'UserDeviceMemorySize' + # uint 'UserDeviceHeapSize' + string 'UserCountry' +end + +message 91, 'IOSSessionEnd' do + uint 'Timestamp' +end + +message 92, 'IOSMetadata' do + uint 'Timestamp' + uint 'Length' + string 'Key' + string 'Value' +end + +message 93, 'IOSCustomEvent', :seq_index => true, :replayer => true do + uint 'Timestamp' + uint 'Length' + string 'Name' + string 'Payload' +end + +message 94, 'IOSUserID' do + uint 'Timestamp' + uint 'Length' + string 'Value' +end + +message 95, 'IOSUserAnonymousID' do + uint 'Timestamp' + uint 'Length' + string 'Value' +end + +message 96, 'IOSScreenChanges', :replayer => true do + uint 'Timestamp' + uint 'Length' + uint 'X' + uint 'Y' + uint 'Width' + uint 'Height' +end + +message 97, 'IOSCrash', :seq_index => true do + uint 'Timestamp' + uint 'Length' + string 'Name' + string 'Reason' + string 'Stacktrace' +end + +message 98, 'IOSScreenEnter', :seq_index => true do + uint 'Timestamp' + uint 'Length' + string 'Title' + string 'ViewName' +end + +message 99, 'IOSScreenLeave' do + uint 'Timestamp' + uint 'Length' + string 'Title' + string 'ViewName' +end + +message 100, 'IOSClickEvent', :seq_index => true, :replayer => true do + uint 'Timestamp' + uint 'Length' + string 'Label' + uint 'X' + uint 'Y' +end + +message 101, 'IOSInputEvent', :seq_index => true do + uint 'Timestamp' + uint 'Length' + string 'Value' + boolean 'ValueMasked' + string 'Label' +end + +=begin +Name/Value may be : +"physicalMemory": Total memory in bytes +"processorCount": Total processors in device +?"activeProcessorCount": Number of currently used processors +"systemUptime": Elapsed time (in seconds) since last boot +?"isLowPowerModeEnabled": Possible values (1 or 0) +2/3!"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical) +!"batteryLevel": Possible values (0 .. 100) +"batteryState": Possible values (0:unknown 1:unplugged 2:charging 3:full) +"orientation": Possible values (0unknown 1:portrait 2:portraitUpsideDown 3:landscapeLeft 4:landscapeRight 5:faceUp 6:faceDown) +"mainThreadCPU": Possible values (0 .. 100) +"memoryUsage": Used memory in bytes +=end +message 102, 'IOSPerformanceEvent', :replayer => true, :seq_index => true do + uint 'Timestamp' + uint 'Length' + string 'Name' + uint 'Value' +end + +message 103, 'IOSLog', :replayer => true do + uint 'Timestamp' + uint 'Length' + string 'Severity' # Possible values ("info", "error") + string 'Content' +end + +message 104, 'IOSInternalError' do + uint 'Timestamp' + uint 'Length' + string 'Content' +end + +message 105, 'IOSNetworkCall', :replayer => true, :seq_index => true do + uint 'Timestamp' + uint 'Length' + uint 'Duration' + string 'Headers' + string 'Body' + string 'URL' + boolean 'Success' + string 'Method' + uint 'Status' +end +message 110, 'IOSPerformanceAggregated', :swift => false do + uint 'TimestampStart' + uint 'TimestampEnd' + uint 'MinFPS' + uint 'AvgFPS' + uint 'MaxFPS' + uint 'MinCPU' + uint 'AvgCPU' + uint 'MaxCPU' + uint 'MinMemory' + uint 'AvgMemory' + uint 'MaxMemory' + uint 'MinBattery' + uint 'AvgBattery' + uint 'MaxBattery' +end + +message 111, 'IOSIssueEvent', :seq_index => true do + uint 'Timestamp' + string 'Type' + string 'ContextString' + string 'Context' + string 'Payload' +end diff --git a/mobs/messages.rb b/mobs/messages.rb new file mode 100644 index 000000000..0f97f3db5 --- /dev/null +++ b/mobs/messages.rb @@ -0,0 +1,409 @@ +# Special one for Batch Meta. Message id could define the version +message 80, 'BatchMeta', :replayer => false do + uint 'PageNo' + uint 'FirstIndex' + int 'Timestamp' +end +message 0, 'Timestamp' do + uint 'Timestamp' +end +message 1, 'SessionStart', :js => false, :replayer => false do + uint 'Timestamp' + uint 'ProjectID' + string 'TrackerVersion' + string 'RevID' + string 'UserUUID' + string 'UserAgent' + string 'UserOS' + string 'UserOSVersion' + string 'UserBrowser' + string 'UserBrowserVersion' + string 'UserDevice' + string 'UserDeviceType' + uint 'UserDeviceMemorySize' + uint 'UserDeviceHeapSize' + string 'UserCountry' + string 'UserID' +end +# Depricated (not used) since OpenReplay tracker 3.0.0 +message 2, 'SessionDisconnect', :js => false do + uint 'Timestamp' +end +message 3, 'SessionEnd', :js => false, :replayer => false do + uint 'Timestamp' +end +message 4, 'SetPageLocation' do + string 'URL' + string 'Referrer' + uint 'NavigationStart' +end +message 5, 'SetViewportSize' do + uint 'Width' + uint 'Height' +end +message 6, 'SetViewportScroll' do + int 'X' + int 'Y' +end +message 7, 'CreateDocument' do +end +message 8, 'CreateElementNode' do + uint 'ID' + uint 'ParentID' + uint 'index' + string 'Tag' + boolean 'SVG' +end +message 9, 'CreateTextNode' do + uint 'ID' + uint 'ParentID' + uint 'Index' +end +message 10, 'MoveNode' do + uint 'ID' + uint 'ParentID' + uint 'Index' +end +message 11, 'RemoveNode' do + uint 'ID' +end +message 12, 'SetNodeAttribute' do + uint 'ID' + string 'Name' + string 'Value' +end +message 13, 'RemoveNodeAttribute' do + uint 'ID' + string 'Name' +end +message 14, 'SetNodeData' do + uint 'ID' + string 'Data' +end +# Depricated starting from 5.5.11 in favor of SetStyleData +message 15, 'SetCSSData', :js => false do + uint 'ID' + string 'Data' +end +message 16, 'SetNodeScroll' do + uint 'ID' + int 'X' + int 'Y' +end +message 17, 'SetInputTarget', :replayer => false do + uint 'ID' + string 'Label' +end +message 18, 'SetInputValue' do + uint 'ID' + string 'Value' + int 'Mask' +end +message 19, 'SetInputChecked' do + uint 'ID' + boolean 'Checked' +end +message 20, 'MouseMove' do + uint 'X' + uint 'Y' +end +# Depricated since OpenReplay 1.2.0 +message 21, 'MouseClickDepricated', :js => false, :replayer => false do + uint 'ID' + uint 'HesitationTime' + string 'Label' +end +message 22, 'ConsoleLog' do + string 'Level' + string 'Value' +end +message 23, 'PageLoadTiming', :replayer => false do + uint 'RequestStart' + uint 'ResponseStart' + uint 'ResponseEnd' + uint 'DomContentLoadedEventStart' + uint 'DomContentLoadedEventEnd' + uint 'LoadEventStart' + uint 'LoadEventEnd' + uint 'FirstPaint' + uint 'FirstContentfulPaint' +end +message 24, 'PageRenderTiming', :replayer => false do + uint 'SpeedIndex' + uint 'VisuallyComplete' + uint 'TimeToInteractive' +end +message 25, 'JSException', :replayer => false do + string 'Name' + string 'Message' + string 'Payload' +end +message 26, 'IntegrationEvent', :js => false, :replayer => false do + uint 'Timestamp' + string 'Source' + string 'Name' + string 'Message' + string 'Payload' +end +message 27, 'RawCustomEvent', :replayer => false do + string 'Name' + string 'Payload' +end +message 28, 'UserID', :replayer => false do + string 'ID' +end +message 29, 'UserAnonymousID', :replayer => false do + string 'ID' +end +message 30, 'Metadata', :replayer => false do + string 'Key' + string 'Value' +end +message 31, 'PageEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'URL' + string 'Referrer' + boolean 'Loaded' + uint 'RequestStart' + uint 'ResponseStart' + uint 'ResponseEnd' + uint 'DomContentLoadedEventStart' + uint 'DomContentLoadedEventEnd' + uint 'LoadEventStart' + uint 'LoadEventEnd' + uint 'FirstPaint' + uint 'FirstContentfulPaint' + uint 'SpeedIndex' + uint 'VisuallyComplete' + uint 'TimeToInteractive' +end +message 32, 'InputEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'Value' + boolean 'ValueMasked' + string 'Label' +end +message 33, 'ClickEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + uint 'HesitationTime' + string 'Label' + string 'Selector' +end +message 34, 'ErrorEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'Source' + string 'Name' + string 'Message' + string 'Payload' +end +message 35, 'ResourceEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + uint 'Duration' + uint 'TTFB' + uint 'HeaderSize' + uint 'EncodedBodySize' + uint 'DecodedBodySize' + string 'URL' + string 'Type' + boolean 'Success' + string 'Method' + uint 'Status' +end +message 36, 'CustomEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'Name' + string 'Payload' +end + + +message 37, 'CSSInsertRule' do + uint 'ID' + string 'Rule' + uint 'Index' +end +message 38, 'CSSDeleteRule' do + uint 'ID' + uint 'Index' +end + +message 39, 'Fetch' do + string 'Method' + string 'URL' + string 'Request' + string 'Response' + uint 'Status' + uint 'Timestamp' + uint 'Duration' +end +message 40, 'Profiler' do + string 'Name' + uint 'Duration' + string 'Args' + string 'Result' +end + +message 41, 'OTable' do + string 'Key' + string 'Value' +end +message 42, 'StateAction', :replayer => false do + string 'Type' +end +message 43, 'StateActionEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'Type' +end + +message 44, 'Redux' do + string 'Action' + string 'State' + uint 'Duration' +end +message 45, 'Vuex' do + string 'Mutation' + string 'State' +end +message 46, 'MobX' do + string 'Type' + string 'Payload' +end +message 47, 'NgRx' do + string 'Action' + string 'State' + uint 'Duration' +end +message 48, 'GraphQL' do + string 'OperationKind' + string 'OperationName' + string 'Variables' + string 'Response' +end +message 49, 'PerformanceTrack' do + int 'Frames' + int 'Ticks' + uint 'TotalJSHeapSize' + uint 'UsedJSHeapSize' +end +message 50, 'GraphQLEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'OperationKind' + string 'OperationName' + string 'Variables' + string 'Response' +end +message 51, 'FetchEvent', :js => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'Method' + string 'URL' + string 'Request' + string 'Response' + uint 'Status' + uint 'Duration' +end +message 52, 'DOMDrop', :js => false, :replayer => false do + uint 'Timestamp' +end +message 53, 'ResourceTiming', :replayer => false do + uint 'Timestamp' + uint 'Duration' + uint 'TTFB' + uint 'HeaderSize' + uint 'EncodedBodySize' + uint 'DecodedBodySize' + string 'URL' + string 'Initiator' +end +message 54, 'ConnectionInformation' do + uint 'Downlink' + string 'Type' +end +message 55, 'SetPageVisibility' do + boolean 'hidden' +end +message 56, 'PerformanceTrackAggr', :js => false, :replayer => false do + uint 'TimestampStart' + uint 'TimestampEnd' + uint 'MinFPS' + uint 'AvgFPS' + uint 'MaxFPS' + uint 'MinCPU' + uint 'AvgCPU' + uint 'MaxCPU' + uint 'MinTotalJSHeapSize' + uint 'AvgTotalJSHeapSize' + uint 'MaxTotalJSHeapSize' + uint 'MinUsedJSHeapSize' + uint 'AvgUsedJSHeapSize' + uint 'MaxUsedJSHeapSize' +end +message 59, 'LongTask' do + uint 'Timestamp' + uint 'Duration' + uint 'Context' + uint 'ContainerType' + string 'ContainerSrc' + string 'ContainerId' + string 'ContainerName' +end +message 60, 'SetNodeAttributeURLBased', :replayer => false do + uint 'ID' + string 'Name' + string 'Value' + string 'BaseURL' +end +# Might replace SetCSSData (although BaseURL is useless after rewriting) +message 61, 'SetCSSDataURLBased', :replayer => false do + uint 'ID' + string 'Data' + string 'BaseURL' +end +message 62, 'IssueEvent', :replayer => false, :js => false do + uint 'MessageID' + uint 'Timestamp' + string 'Type' + string 'ContextString' + string 'Context' + string 'Payload' +end +message 63, 'TechnicalInfo', :replayer => false do + string 'Type' + string 'Value' +end +message 64, 'CustomIssue', :replayer => false do + string 'Name' + string 'Payload' +end +# Since 5.6.6; only for websocket (might be probably replaced with ws.close()) +# Depricated +message 65, 'PageClose', :replayer => false do +end +message 66, 'AssetCache', :replayer => false, :js => false do + string 'URL' +end +message 67, 'CSSInsertRuleURLBased', :replayer => false do + uint 'ID' + string 'Rule' + uint 'Index' + string 'BaseURL' +end +message 69, 'MouseClick' do + uint 'ID' + uint 'HesitationTime' + string 'Label' + string 'Selector' +end + +# Since 3.4.0 +message 70, 'CreateIFrameDocument' do + uint 'FrameID' + uint 'ID' +end \ No newline at end of file diff --git a/mobs/primitives/primitives.go b/mobs/primitives/primitives.go new file mode 100644 index 000000000..2603d31f5 --- /dev/null +++ b/mobs/primitives/primitives.go @@ -0,0 +1,124 @@ +package messages + +import ( + "errors" + "io" +) + +func ReadByte(reader io.Reader) (byte, error) { + p := make([]byte, 1) + _, err := io.ReadFull(reader, p) + if err != nil { + return 0, err + } + return p[0], nil +} + +func SkipBytes(reader io.ReadSeeker) error { + n, err := ReadUint(reader) + if err != nil { + return err + } + _, err := reader.Seek(n, io.SeekCurrent); + return err +} + +func ReadData(reader io.Reader) ([]byte, error) { + n, err := ReadUint(reader) + if err != nil { + return nil, err + } + p := make([]byte, n) + _, err := io.ReadFull(reader, p) + if err != nil { + return nil, err + } + return p, nil +} + +func ReadUint(reader io.Reader) (uint64, error) { + var x uint64 + var s uint + i := 0 + for { + b, err := ReadByte(reader) + if err != nil { + return x, err + } + if b < 0x80 { + if i > 9 || i == 9 && b > 1 { + return x, errors.New("overflow") + } + return x | uint64(b)<> 1) + if err != nil { + return x, err + } + if ux&1 != 0 { + x = ^x + } + return x, err +} + +func ReadBoolean(reader io.Reader) (bool, error) { + p := make([]byte, 1) + _, err := io.ReadFull(reader, p) + if err != nil { + return false, err + } + return p[0] == 1, nil +} + +func ReadString(reader io.Reader) (string, error) { + l, err := ReadUint(reader) + if err != nil { + return "", err + } + buf := make([]byte, l) + _, err = io.ReadFull(reader, buf) + if err != nil { + return "", err + } + return string(buf), nil +} + +func WriteUint(v uint64, buf []byte, p int) int { + for v >= 0x80 { + buf[p] = byte(v) | 0x80 + v >>= 7 + p++ + } + buf[p] = byte(v) + return p + 1 +} + +func WriteInt(v int64, buf []byte, p int) int { + uv := uint64(v) << 1 + if v < 0 { + uv = ^uv + } + return WriteUint(uv, buf, p) +} + +func WriteBoolean(v bool, buf []byte, p int) int { + if v { + buf[p] = 1 + } else { + buf[p] = 0 + } + return p + 1 +} + +func WriteString(str string, buf []byte, p int) int { + p = WriteUint(uint64(len(str)), buf, p) + return p + copy(buf[p:], str) +} diff --git a/mobs/primitives/primitives.py b/mobs/primitives/primitives.py new file mode 100644 index 000000000..5aeb0e4ed --- /dev/null +++ b/mobs/primitives/primitives.py @@ -0,0 +1,62 @@ +import io + +class Codec: + """ + Implements encode/decode primitives + """ + + @staticmethod + def read_boolean(reader: io.BytesIO): + b = reader.read(1) + return b == 1 + + @staticmethod + def read_uint(reader: io.BytesIO): + """ + The ending "big" doesn't play any role here, + since we're dealing with data per one byte + """ + x = 0 # the result + s = 0 # the shift (our result is big-ending) + i = 0 # n of byte (max 9 for uint64) + while True: + b = reader.read(1) + num = int.from_bytes(b, "big", signed=False) + # print(i, x) + + if num < 0x80: + if i > 9 | i == 9 & num > 1: + raise OverflowError() + return int(x | num << s) + x |= (num & 0x7f) << s + s += 7 + i += 1 + + @staticmethod + def read_int(reader: io.BytesIO) -> int: + """ + ux, err := ReadUint(reader) + x := int64(ux >> 1) + if err != nil { + return x, err + } + if ux&1 != 0 { + x = ^x + } + return x, err + """ + ux = Codec.read_uint(reader) + x = int(ux >> 1) + + if ux & 1 != 0: + x = - x - 1 + return x + + @staticmethod + def read_string(reader: io.BytesIO) -> str: + length = Codec.read_uint(reader) + s = reader.read(length) + try: + return s.decode("utf-8", errors="replace").replace("\x00", "\uFFFD") + except UnicodeDecodeError: + return None diff --git a/mobs/primitives/primitives.swift b/mobs/primitives/primitives.swift new file mode 100644 index 000000000..b7d66d0f9 --- /dev/null +++ b/mobs/primitives/primitives.swift @@ -0,0 +1,60 @@ +extension Data { + func readByte(offset: inout Int) -> UInt8 { + if offset >= self.count { + fatalError(">>> Error reading Byte") + } + let b = self[offset] + offset += 1 + return b + } + func readUint(offset: inout Int) -> UInt64 { + var x: UInt64 = 0 + var s: Int = 0 + var i: Int = 0 + while true { + let b = readByte(offset: &offset) + if b < 0x80 { + if i > 9 || i == 9 && b > 1 { + fatalError(">>> Error reading UInt") + } + return x | UInt64(b)< Int64 { + let ux = readUint(offset: &offset) + var x = Int64(ux >> 1) + if ux&1 != 0 { + x = ~x + } + return x + } + func readBoolean(offset: inout Int) -> Bool { + return readByte(offset: &offset) == 1 + } + mutating func writeUint(_ input: UInt64) { + var v = input + while v >= 0x80 { + append(UInt8(v.littleEndian & 0x7F) | 0x80) // v.littleEndian ? + v >>= 7 + } + append(UInt8(v)) + } + mutating func writeInt(_ v: Int64) { + var uv = UInt64(v) << 1 + if v < 0 { + uv = ~uv + } + writeUint(uv) + } + mutating func writeBoolean(_ v: Bool) { + if v { + append(1) + } else { + append(0) + } + } +} \ No newline at end of file diff --git a/mobs/run.rb b/mobs/run.rb new file mode 100644 index 000000000..ed9999e88 --- /dev/null +++ b/mobs/run.rb @@ -0,0 +1,136 @@ +require 'erb' + + +# TODO: change method names to correct (CapitalCase and camelCase, not CamalCase and firstLower) +class String + def camel_case + return self if self !~ /_/ && self =~ /[A-Z]+.*/ + split('_').map{|e| e.capitalize}.join.upperize + end + + def camel_case_lower + self.split('_').inject([]){ |buffer,e| buffer.push(buffer.empty? ? e : e.capitalize) }.join.upperize + end + + def upperize + self.sub('Id', 'ID').sub('Url', 'URL') + end + + def first_lower + self.sub(/^[A-Z]+/) {|f| f.downcase } + end + + def underscore + self.gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end +end + +class Attribute + attr_reader :name, :type + def initialize(name:, type:) + @name = name + @type = type + end + + def type_js + case @type + when :int + "number" + when :uint + "number" + when :json + # TODO + # raise "Unexpected attribute type: data type attribute #{@name} found in JS template" + "string" + when :data + raise "Unexpected attribute type: data type attribute #{@name} found in JS template" + else + @type + end + end + + def type_go + case @type + when :int + 'int64' + when :uint + 'uint64' + when :string + 'string' + when :data + '[]byte' + when :boolean + 'bool' + when :json + 'interface{}' + end + end + + def lengh_encoded + case @type + when :string, :data + true + else + false + end + end + +end + + +$context = :web + +class Message + attr_reader :id, :name, :js, :replayer, :swift, :seq_index, :attributes, :context + def initialize(name:, id:, js: $context == :web, replayer: $context == :web, swift: $context == :ios, seq_index: false, &block) + @id = id + @name = name + @js = js + @replayer = replayer + @swift = swift + @seq_index = seq_index + @context = $context + @attributes = [] + instance_eval &block + end + + %i(int uint boolean string data).each do |type| + define_method type do |name, opts = {}| + opts.merge!( + name: name, + type: type, + ) + @attributes << Attribute.new(opts) + end + end +end + +$ids = [] +$messages = [] +def message(id, name, opts = {}, &block) + raise "id duplicated #{name}" if $ids.include? id + raise "id is too big #{name}" if id > 120 + $ids << id + opts[:id] = id + opts[:name] = name + msg = Message.new(opts, &block) + $messages << msg +end + +require './messages.rb' + +$context = :ios +require './ios_messages.rb' + +Dir["templates/*.erb"].each do |tpl| + e = ERB.new(File.read(tpl)) + path = tpl.split '/' + t = '../' + path[1].gsub('~', '/') + t = t[0..-5] + File.write(t, e.result) + puts tpl + ' --> ' + t +end diff --git a/mobs/templates/backend~pkg~messages~filters.go.erb b/mobs/templates/backend~pkg~messages~filters.go.erb new file mode 100644 index 000000000..ac4ba9cba --- /dev/null +++ b/mobs/templates/backend~pkg~messages~filters.go.erb @@ -0,0 +1,10 @@ +// Auto-generated, do not edit +package messages + +func IsReplayerType(id int) bool { + return <%= $messages.select { |msg| msg.replayer }.map{ |msg| "#{msg.id} == id" }.join(' || ') %> +} + +func IsIOSType(id int) bool { + return <%= $messages.select { |msg| msg.context == :ios }.map{ |msg| "#{msg.id} == id"}.join(' || ') %> +} diff --git a/mobs/templates/backend~pkg~messages~get-timestamp.go.erb b/mobs/templates/backend~pkg~messages~get-timestamp.go.erb new file mode 100644 index 000000000..f4a634dea --- /dev/null +++ b/mobs/templates/backend~pkg~messages~get-timestamp.go.erb @@ -0,0 +1,12 @@ +// Auto-generated, do not edit +package messages + +func GetTimestamp(message Message) uint64 { + switch msg := message.(type) { +<% $messages.select { |msg| msg.swift }.each do |msg| %> + case *<%= msg.name %>: + return msg.Timestamp +<% end %> + } + return uint64(message.Meta().Timestamp) +} diff --git a/mobs/templates/backend~pkg~messages~messages.go.erb b/mobs/templates/backend~pkg~messages~messages.go.erb new file mode 100644 index 000000000..81fb18800 --- /dev/null +++ b/mobs/templates/backend~pkg~messages~messages.go.erb @@ -0,0 +1,22 @@ +// Auto-generated, do not edit +package messages +<% $messages.each do |msg| %> +type <%= msg.name %> struct { + message +<%= msg.attributes.map { |attr| +" #{attr.name} #{attr.type_go}" }.join "\n" %> +} + +func (msg *<%= msg.name %>) Encode() []byte { + buf := make([]byte, <%= msg.attributes.count * 10 + 1 %><%= msg.attributes.map { |attr| "+len(msg.#{attr.name})" if attr.lengh_encoded }.compact.join %>) + buf[0] = <%= msg.id %> + p := 1 +<%= msg.attributes.map { |attr| +" p = Write#{attr.type.to_s.camel_case}(msg.#{attr.name}, buf, p)" }.join "\n" %> + return buf[:p] +} + +func (msg *<%= msg.name %>) TypeID() int { + return <%= msg.id %> +} +<% end %> diff --git a/mobs/templates/backend~pkg~messages~read-message.go.erb b/mobs/templates/backend~pkg~messages~read-message.go.erb new file mode 100644 index 000000000..2e8920747 --- /dev/null +++ b/mobs/templates/backend~pkg~messages~read-message.go.erb @@ -0,0 +1,26 @@ +// Auto-generated, do not edit +package messages + +import ( + "fmt" + "io" +) + +func ReadMessage(reader io.Reader) (Message, error) { + t, err := ReadUint(reader) + if err != nil { + return nil, err + } + switch t { +<% $messages.each do |msg| %> + case <%= msg.id %>: + msg := &<%= msg.name %>{} +<%= msg.attributes.map { |attr| +" if msg.#{attr.name}, err = Read#{attr.type.to_s.camel_case}(reader); err != nil { + return nil, err + }" }.join "\n" %> + return msg, nil +<% end %> + } + return nil, fmt.Errorf("Unknown message code: %v", t) +} diff --git a/mobs/templates/frontend~app~player~MessageDistributor~messages~RawMessageReader.ts.erb b/mobs/templates/frontend~app~player~MessageDistributor~messages~RawMessageReader.ts.erb new file mode 100644 index 000000000..337dfb803 --- /dev/null +++ b/mobs/templates/frontend~app~player~MessageDistributor~messages~RawMessageReader.ts.erb @@ -0,0 +1,35 @@ +// Auto-generated, do not edit + +import PrimitiveReader from './PrimitiveReader' +import type { RawMessage } from './raw' + + +export default class RawMessageReader extends PrimitiveReader { + readMessage(): RawMessage | null { + const p = this.p + const resetPointer = () => { + this.p = p + return null + } + + const tp = this.readUint() + if (tp === null) { return resetPointer() } + + switch (tp) { + <% $messages.select { |msg| msg.js || msg.replayer }.each do |msg| %> + case <%= msg.id %>: { +<%= msg.attributes.map { |attr| +" const #{attr.name.first_lower} = this.read#{attr.type.to_s.camel_case}(); if (#{attr.name.first_lower} === null) { return resetPointer() }" }.join "\n" %> + return { + tp: "<%= msg.name.underscore %>", +<%= msg.attributes.map { |attr| +" #{attr.name.first_lower}," }.join "\n" %> + }; + } + <% end %> + default: + throw new Error(`Unrecognizable message type: ${ tp }; Pointer at the position ${this.p} of ${this.buf.length}`) + return null; + } + } +} diff --git a/mobs/templates/frontend~app~player~MessageDistributor~messages~message.ts.erb b/mobs/templates/frontend~app~player~MessageDistributor~messages~message.ts.erb new file mode 100644 index 000000000..e81bddd7c --- /dev/null +++ b/mobs/templates/frontend~app~player~MessageDistributor~messages~message.ts.erb @@ -0,0 +1,13 @@ +// Auto-generated, do not edit + +import type { Timed } from './timed' +import type { RawMessage } from './raw' +import type { +<%= $messages.select { |msg| msg.js || msg.replayer }.map { |msg| " Raw#{msg.name.underscore.camel_case}," }.join "\n" %> +} from './raw' + +export type Message = RawMessage & Timed + +<% $messages.select { |msg| msg.js || msg.replayer }.each do |msg| %> +export type <%= msg.name.underscore.camel_case %> = Raw<%= msg.name.underscore.camel_case %> & Timed +<% end %> \ No newline at end of file diff --git a/mobs/templates/frontend~app~player~MessageDistributor~messages~raw.ts.erb b/mobs/templates/frontend~app~player~MessageDistributor~messages~raw.ts.erb new file mode 100644 index 000000000..bb441f810 --- /dev/null +++ b/mobs/templates/frontend~app~player~MessageDistributor~messages~raw.ts.erb @@ -0,0 +1,14 @@ +// Auto-generated, do not edit + +export const TP_MAP = { +<%= $messages.select { |msg| msg.js || msg.replayer }.map { |msg| " #{msg.id}: \"#{msg.name.underscore}\"," }.join "\n" %> +} + +<% $messages.select { |msg| msg.js || msg.replayer }.each do |msg| %> +export interface Raw<%= msg.name.underscore.camel_case %> { + tp: "<%= msg.name.underscore %>", +<%= msg.attributes.map { |attr| " #{attr.name.first_lower}: #{attr.type_js}," }.join "\n" %> +} +<% end %> + +export type RawMessage = <%= $messages.select { |msg| msg.js || msg.replayer }.map { |msg| "Raw#{msg.name.underscore.camel_case}" }.join " | " %>; diff --git a/mobs/templates/ios/ASMessage.swift b/mobs/templates/ios/ASMessage.swift new file mode 100644 index 000000000..664a65643 --- /dev/null +++ b/mobs/templates/ios/ASMessage.swift @@ -0,0 +1,36 @@ +// Auto-generated, do not edit +import UIKit + +enum ASMessageType: UInt64 { +<%= $messages.map { |msg| " case #{msg.name.first_lower} = #{msg.id}" }.join "\n" %> +} +<% $messages.each do |msg| %> +class AS<%= msg.name.to_s.camel_case %>: ASMessage { +<%= msg.attributes[2..-1].map { |attr| " let #{attr.property}: #{attr.type_swift}" }.join "\n" %> + + init(<%= msg.attributes[2..-1].map { |attr| "#{attr.property}: #{attr.type_swift}" }.join ", " %>) { +<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = #{attr.property}" }.join "\n" %> + super.init(messageType: .<%= "#{msg.name.first_lower}" %>) + } + + override init?(genericMessage: GenericMessage) { + <% if msg.attributes.length > 2 %> do { + var offset = 0 +<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = try genericMessage.body.read#{attr.type_swift_read}(offset: &offset)" }.join "\n" %> + super.init(genericMessage: genericMessage) + } catch { + return nil + } + <% else %> + super.init(genericMessage: genericMessage) + <% end %>} + + override func contentData() -> Data { + return Data(values: UInt64(<%= "#{msg.id}"%>), timestamp<% if msg.attributes.length > 2 %>, Data(values: <%= msg.attributes[2..-1].map { |attr| attr.property }.join ", "%>)<% end %>) + } + + override var description: String { + return "-->> <%= msg.name.to_s.camel_case %>(<%= "#{msg.id}"%>): timestamp:\(timestamp) <%= msg.attributes[2..-1].map { |attr| "#{attr.property}:\\(#{attr.property})" }.join " "%>"; + } +} +<% end %> diff --git a/mobs/templates/tracker~tracker~src~common~messages.ts.erb b/mobs/templates/tracker~tracker~src~common~messages.ts.erb new file mode 100644 index 000000000..3b3849370 --- /dev/null +++ b/mobs/templates/tracker~tracker~src~common~messages.ts.erb @@ -0,0 +1,31 @@ +// Auto-generated, do not edit +import type { Writer, Message }from "./types.js"; +export default Message + +function bindNew( + Class: C & { new(...args: A): T } +): C & ((...args: A) => T) { + function _Class(...args: A) { + return new Class(...args); + } + _Class.prototype = Class.prototype; + return T)>_Class; +} + +export const classes: Map = new Map(); + +<% $messages.select { |msg| msg.js }.each do |msg| %> +class _<%= msg.name %> implements Message { + readonly _id: number = <%= msg.id %>; + constructor( + <%= msg.attributes.map { |attr| "public #{attr.name.first_lower}: #{attr.type_js}" }.join ",\n " %> + ) {} + encode(writer: Writer): boolean { + return writer.uint(<%= msg.id %>)<%= " &&" if msg.attributes.length() > 0 %> + <%= msg.attributes.map { |attr| "writer.#{attr.type}(this.#{attr.name.first_lower})" }.join " &&\n " %>; + } +} +export const <%= msg.name %> = bindNew(_<%= msg.name %>); +classes.set(<%= msg.id %>, <%= msg.name %>); + +<% end %>