#!/bin/bash # FIELD_TYPES_STR=(string str text) # FIELD_TYPES_INT=(integer int num number) # FIELD_TYPES_RAW=(raw data bytes) # works but we do not want to show all to the user # IFS=" " read -r -a FIELD_TYPES <<< "${FIELD_TYPES_INT[*]} ${FIELD_TYPES_STR[*]} ${FIELD_TYPES_RAW[*]}" # IFS=$'\n' # FIELD_TYPES=(int str raw) declare -A FIELD_TYPES=( [int]='int integer num number' [string]='string str text' [raw]='raw data bytes' [bool]='bool boolean' ) function show_help() { echo "usage: twnet [action] [options]" echo "options:" echo " --help | -h show this help" echo "actions:" echo " generate | g generate a ruby file. See $(tput bold)twnet g --help$(tput sgr0)" } # TODO: move those file helpers to a lib file and also use them in hooks.sh function replace_line() { local filename="$1" local search_ln="$2" local replace_str="$3" _edit_line_in_file replace 0 "$filename" "$search_ln" "$replace_str" } # replace 'num_lines' after match function replace_lines_from() { local filename="$1" local search_ln="$2" local replace_str="$3" local num_lines="$4" _edit_line_in_file replace "$num_lines" "$filename" "$search_ln" "$replace_str" } function append_line() { local filename="$1" local search_ln="$2" local replace_str="$3" _edit_line_in_file append 0 "$filename" "$search_ln" "$replace_str" } function _edit_line_in_file() { local mode="$1" local num_lines_from="$2" local filename="$3" local search_ln="$4" local replace_str="$5" local repl_ln local from_ln local to_ln if [[ ! "$num_lines_from" =~ ^[0-9]+$ ]] then echo "Error: _edit_line_in_file num_lines_from '$num_lines_from' has to be numeric" exit 1 fi repl_ln="$(grep -nF "$search_ln" "$filename" | cut -d':' -f1)" if [ "$mode" == "append" ] then from_ln="$((repl_ln+1))" to_ln="$repl_ln" elif [ "$mode" == "replace" ] then from_ln="$((repl_ln-1))" to_ln="$((repl_ln+1+num_lines_from))" else echo "Error: _edit_line_in_file expectes mode replace or append" exit 1 fi if [ "$repl_ln" == "" ] then echo "Error: failed to get line of '$search_ln' in file '$filename'" exit 1 fi { head -n "$from_ln" "$filename" echo -ne "$replace_str" tail -n +"$to_ln" "$filename" } > "$filename".tmp mv "$filename".tmp "$filename" } function is_camel_case() { [[ "$1" =~ ^([A-Z][a-z]+)+$ ]] && return 0 return 1 } function is_snake_case() { [[ "$1" =~ ^[a-z][a-z_]*[a-z]$ ]] && return 0 return 1 } function split_camel_case() { local str="$1" local words=() local word='' while read -n1 -r c do if [[ "$c" =~ [A-Z] ]] then if [ "$word" != "" ] then words+=("${word,}") word='' fi fi word+="$c" done < <(echo -n "$str") if [ "$word" != "" ] then words+=("${word,}") word='' fi echo "${words[*]}" } function split_snake_case() { local str="$1" local words=() local word='' while read -n1 -r c do if [[ "$c" == '_' ]] then if [ "$word" != "" ] then words+=("${word,}") word='' fi else word+="$c" fi done < <(echo -n "$str") if [ "$word" != "" ] then words+=("${word,}") word='' fi echo "${words[*]}" } function camel_to_snake_case() { local camel="$1" local w local snake='' for w in $(split_camel_case "$camel") do snake+="_$w" done echo "${snake:1}" } function snake_to_camel_case() { local snake="$1" local w local camel='' for w in $(split_snake_case "$snake") do camel+="${w^}" done echo "$camel" } function action_generate_help() { echo "usage: twnet generate [fields..]" echo "options:" echo " --help | -h show this help" echo "type:" echo " server_packet | srv_pck | sp packet sent from server to client" echo " client_packet | cl_pck | cp packet sent from client to server" echo "name:" echo " will be the packet name use UpperCamelCase" echo " for example ClSay" echo "fields:" echo " the formate is field_name:field_type" echo " the allowed types are: ${!FIELD_TYPES[*]}" echo " examples:" echo " target_id:int" echo " message:str" echo "examples:" echo " generate a cl_say.rb file describing a packet that" echo " is sent from the client to the server has the class name ClSay" echo " and two fields first target_id (integer) then message (string)" echo "" tput bold echo " twnet generate cp ClSay target_id:int message:str" tput sgr0 } function replace_str() { local filename="$1" local search="$2" local replace="$3" sed "s/$search/$replace/" "$filename" > "$filename".tmp mv "$filename".tmp "$filename" } function action_generate() { local arg local arg_type='' local arg_name='' local name_camel local name_snake local field_name local field_type # associative array does not work # because it does not keep order local fields=() local valid_type if [ "$#" -eq "0" ] then action_generate_help exit 1 fi while true do [[ "$#" -eq "0" ]] && break arg="$1" shift if [ "${arg::1}" == "-" ] then if [ "$arg" == "--help" ] || [ "$arg" == "-h" ] then action_generate_help exit 0 fi else if [ "$arg_type" == "" ] then if [ "$arg" == "server_packet" ] || [ "$arg" == "srv_pck" ] || [ "$arg" == "sv_pck" ] || [ "$arg" == "sp" ] || [ "$arg" == "sv" ] then arg_type=server elif [ "$arg" == "client_packet" ] || [ "$arg" == "cl_pck" ] || [ "$arg" == "cp" ] || [ "$arg" == "cl" ] then arg_type=client else echo "Error: invalid packet type '$arg'" echo " expected: server_packet or client_packet" exit 1 fi elif [ "$arg_name" == "" ] then arg_name="$arg" # tried to be smart with regex and failed # if [[ "$arg_name" =~ ([A-Z][a-z]*)+ ]] # then # for b in "${BASH_REMATCH[@]}" # do # echo "b: $b" # done # else # echo "Error: name '$arg_name' has to be UpperCamelCase" # exit 1 # fi if ! is_camel_case "$arg_name" then echo "Error: name '$arg_name' has to be UpperCamelCase" exit 1 fi name_camel="$arg_name" name_snake="$(camel_to_snake_case "$arg_name")" elif [[ "$arg" =~ (.*):(.*) ]] then field_name="${BASH_REMATCH[1]}" field_type="${BASH_REMATCH[2]}" local valid=0 for valid_type in "${!FIELD_TYPES[@]}" do local tt for tt in ${FIELD_TYPES[$valid_type]} do [[ "$tt" == "$field_type" ]] || continue field_type="$valid_type" valid=1 done done if [ "$valid" == "0" ] then echo "Error: '$field_type' is not a valid field type" echo " valid types are: ${FIELD_TYPES[*]}" exit 1 fi if ! is_snake_case "$field_name" then echo "Error: '$field_name' is not a valid field name" echo " field names have to be lower_snake_case" exit 1 fi fields+=("$field_name:$field_type") else echo "Error: unkown argument '$arg' try $(tput bold)twnet g --help$(tput sgr0)" exit 1 fi fi done if [ "$arg_type" == "" ] then echo "Error: type can not be empty $(tput bold)twnet g --help$(tput sgr0)" exit 1 fi if [ "$arg_name" == "" ] then echo "Error: name can not be empty $(tput bold)twnet g --help$(tput sgr0)" exit 1 fi local tmpdir tmpdir=scripts/tmp mkdir -p scripts/tmp local tmpfile tmpfile="$tmpdir/$name_snake.rb" cp scripts/packet_template.rb "$tmpfile" replace_str "$tmpfile" PacketName "$name_camel" replace_str "$tmpfile" SENDER "${arg_type^}" if [ "$arg_type" == "client" ] then replace_str "$tmpfile" RECEIVER Server else replace_str "$tmpfile" RECEIVER Client fi local accessors='' while IFS=':' read -r field_name field_type do accessors+=", :$field_name" done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' replace_line "$tmpfile" attr_accessor " attr_accessor${accessors:1}\n" local unpacks='' while IFS=':' read -r field_name field_type do if [ "$field_type" == "bool" ] then unpacks+="\n @$field_name = u.get_int" else unpacks+="\n @$field_name = u.get_$field_type" fi done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' replace_line "$tmpfile" Unpacker.new " u = Unpacker.new(data)$unpacks\n" local hashs='' while IFS=':' read -r field_name field_type do if [ "$field_type" == "raw" ] then # TODO: pick nice raw default # after this is closed # https://github.com/ChillerDragon/teeworlds_network/issues/15 hashs+="\n @$field_name = attr[:$field_name] || 'RAAAAAAAAAW DATA !!!!!!'" elif [ "$field_type" == "int" ] then hashs+="\n @$field_name = attr[:$field_name] || 0" elif [ "$field_type" == "bool" ] then hashs+="\n @$field_name = attr[:$field_name] || false" else # string or other hashs+="\n @$field_name = attr[:$field_name] || 'TODO: fill default'" fi done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' replace_line "$tmpfile" "@foo = attr[:foo] || 0" "${hashs:2}\n" hashs=' {' while IFS=':' read -r field_name field_type do hashs+="\n $field_name: @$field_name," done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' hashs="${hashs::-1}" hashs+="\n }" replace_line "$tmpfile" "{ foo: @foo," "${hashs:2}\n" local packs='' while IFS=':' read -r field_name field_type do packs+=" " if [ "$field_type" == "raw" ] then packs+="Packer.pack_raw(@$field_name) +\n" elif [ "$field_type" == "int" ] || [ "$field_type" == "bool" ] then packs+="Packer.pack_int(@$field_name) +\n" else # string or other packs+="Packer.pack_str(@$field_name) +\n" fi done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' replace_line "$tmpfile" Packer.pack_int "${packs:2:-4}\n" local bools='' while IFS=':' read -r field_name field_type do [[ "$field_type" == "bool" ]] || continue bools+=" def $field_name?\n" bools+=" !@$field_name.zero?\n" bools+=" end\n" bools+="\n" done < <(echo "${fields[@]}" | tr ' ' '\n') IFS=$'\n' if [ "$bools" == "" ] then replace_lines_from "$tmpfile" 'def foo?' '' 3 else replace_lines_from "$tmpfile" 'def foo?' "${bools::-2}\n" 3 fi local destfile destfile="lib/messages/$(basename "$tmpfile")" if [ -f "$destfile" ] then echo "Error: file already exists '$destfile'" exit 1 fi mv "$tmpfile" "$destfile" } function parse_args() { local arg while true do [[ "$#" -eq "0" ]] && break arg="$1" shift if [ "${arg::1}" == "-" ] then if [ "$arg" == "--help" ] || [ "$arg" == "-h" ] then show_help exit 0 fi else if [ "$arg" == "generate" ] || [ "$arg" == "g" ] then action_generate "$@" exit else echo "Error: unkown action '$arg'" exit 1 fi fi done } if [ "$#" -eq "0" ] then show_help exit 1 fi parse_args "$@"