From 7ec21fd3086986055fd89fb9d4f960771b406809 Mon Sep 17 00:00:00 2001 From: mewrrythekibby Date: Thu, 12 Feb 2026 13:46:36 -0600 Subject: [PATCH] Add savefile and modeline :3 --- mewny.rb | 359 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 266 insertions(+), 93 deletions(-) diff --git a/mewny.rb b/mewny.rb index 109d026..de50a77 100755 --- a/mewny.rb +++ b/mewny.rb @@ -7,6 +7,19 @@ require 'optparse' PUBLIC_HOST = '0.0.0.0' PRIVATE_HOST = '127.0.0.1' DEFAULT_CLIENT_HOST = '127.0.0.1' +VERSION = { + :SRV_TXT => '0.01', + :CLN_TXT => '0.01', + :CLN_ARO => '0.02', + :SRV_CO8 => '0.30', # 8-color + :CLN_CO8 => '0.30', # 8-color + :SRV_GSZ => '0.40', + + :FIL_TXT => '0.01', + :FIL_CUP => '0.02', + :FIL_CO8 => '0.30', +} + options = { :port => 7570, :server => false, @@ -14,6 +27,9 @@ options = { :server_host => PRIVATE_HOST, :rows => 24, :cols => 40, + :file => nil, + :file_lockout => false, + :file_interval => 600, } OptionParser.new do |opt| opt.on('-pPORT', '--port PORT', Integer, @@ -34,6 +50,11 @@ OptionParser.new do |opt| '(client only) Host to connect to (defaults to `localhost`)') do |o| options[:host] = o end + opt.on('-tSECONDS', '--save-interval SECONDS', Float, + '(server only) How long to wait between file writes in seconds. (defaults to 600 which is 10 minutes)' + ) do |o| + options[:file_interval] = o + end opt.on('-rROWS', '--rows ROWS', Integer, '(server only) The number of rows to have in the shared buffer' ) do |o| @@ -58,11 +79,49 @@ OptionParser.new do |opt| ) do options[:server_host] = PRIVATE_HOST end + opt.on('-f FILE', '--save-file FILE', + '(server only) File to read initial state from and save state to.' + ) do |f| + break if options[:file_lockout] + options[:file_lockout] = true + file_stuff = { + :name => f, + :text => '', + :color => [], + :cursor => 0, + :writeme => false + } + unless File.exist? f + file_stuff[:writeme] = true + options[:file] = file_stuff + options[:file_lockout] = false + next + end + File.readlines(f, chomp: true).each do |msg| + ver, msg = msg.chomp.split('|',2) + next unless msg != nil + case ver + when VERSION[:FIL_TXT] + file_stuff[:text] = msg + when VERSION[:FIL_CO8] + msg.each_char.with_index do |c, i| + next unless c =~ /[0-7]/ + file_stuff[:color] << { + :co8 => 7 + } until file_stuff[:color].length > i + file_stuff[:color][i][:co8] = c.to_i.clamp(0,7) + end + when VERSION[:FIL_CUP] + file_stuff[:cursor] = msg.to_i + end + end + options[:file] = file_stuff + end opt.on('-h', '--help', 'Get help with this command') do puts opt exit end -end.parse!(into: options) +end.parse! HOST = options[:host] SRV_HOST = options[:server_host] @@ -70,15 +129,22 @@ PORT = options[:port] ROWS = options[:rows] COLS = options[:cols] -VERSION = { - :SRV_TXT => '0.01', - :CLN_TXT => '0.01', - :CLN_ARO => '0.02', - :SRV_CO8 => '0.30', # 8-color - :CLN_CO8 => '0.30', # 8-color -} +FILE_INTERVAL = options[:file_interval] + +def do_server(file_opts) + def write_current(wi, buffer, cobuf, file_opts) + return if file_opts.nil? + File.open(file_opts[:name], 'w') do |f| + f.puts("#{VERSION[:FIL_TXT]}|#{clean_chars(buffer)}\n") + f.write("#{VERSION[:FIL_CO8]}|") + cobuf.each do |color| + f.write(color[:co8].to_s) + end + f.puts '' + f.puts("#{VERSION[:FIL_CUP]}|#{wi}") + end + end -def do_server server = TCPServer.new(SRV_HOST, PORT) sockets = [server] sockats = { @@ -88,19 +154,33 @@ def do_server } } } + if file_opts.nil? || file_opts[:writeme] + pre = "#{ROWS}r#{COLS}c >>> " + buffer = pre.ljust(ROWS*COLS)[0...ROWS*COLS] + cobuf = Array.new(ROWS*COLS, { + :co8 => 7 + }) + windex = pre.length # Wipe your buffer clean with your write index + else + buffer = file_opts[:text].ljust(ROWS*COLS)[0...ROWS*COLS] + cobuf = file_opts[:color] + if cobuf.length < ROWS*COLS + cobuf += Array.new(ROWS*COLS - cobuf.length ) { {:co8 => 7} } + else + cobuf = cobuf.first(ROWS*COLS) + end + windex = file_opts[:cursor] + end + windex = windex%buffer.length + if not file_opts.nil? + write_current(windex, buffer, cobuf, file_opts) + end puts "PORT: #{PORT}" puts "GRID SIZE: #{ROWS}r#{COLS}c" if SRV_HOST == PUBLIC_HOST then puts "EXPOSED" end puts 'Listening...' - pre = "#{ROWS}r#{COLS}c >>> " - buffer = pre.ljust(ROWS*COLS)[0...ROWS*COLS] - cobuf = Array.new(ROWS*COLS, { - :co8 => 7 - }) - windex = pre.length # Wipe your buffer clean with your write index - windex = windex%buffer.length motions = [] def new_buf_cob_wi_out(buf, cob, wi, newtext, newcolors, motions: []) @@ -157,88 +237,128 @@ def do_server [buf, cob, wi, out] end - loop do - ready, = IO.select(sockets) - newtext = '' - newcolors = [] - - ready.each do |sck| - if sck == server - client = server.accept - sockats[client] = { - :color => { - :co8 => 7 + begin + old = { + :windex => windex.dup, + :buffer => buffer.dup, + :cobuf => cobuf.dup, + :lastsave => Time.now, + } + Thread.new do + loop do + time_since_next_save = Time.now - old[:lastsave] + time_till_next_save = FILE_INTERVAL - time_since_next_save + # To ensure thread doesn't bog down cpu too much..... + sleep [ + [ + [1.0, # Wait a second + time_till_next_save/2 # or half the remaining time + ].max, # Whichever's longer + time_till_next_save].min, # Unless the remaining time is shorter than a second + 1.0].max # But don't wait longer than no time at all + unless (old[:windex] == windex && + old[:buffer] == buffer && + old[:cobuf] == cobuf) + next if file_opts.nil? + next if time_since_next_save < FILE_INTERVAL + old = { + :lastsave => Time.now, + :windex => windex.dup, + :buffer => buffer.dup, + :cobuf => cobuf.dup } - } - sockets << client - puts "Connection: #{client.peeraddr}" - client.puts new_buf_cob_wi_out(buffer,cobuf,0,buffer,cobuf)[3] - if buffer.length > 1 - client.puts new_buf_cob_wi_out( - buffer,cobuf,windex-1, - buffer[windex-1],[cobuf[windex-1]])[3] + write_current(windex, buffer, cobuf, file_opts) + puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] File autosaved." end - next end - begin - msg = sck.gets - unless msg - puts "Disconnect: #{sck.peeraddr}" - sck.close - sockets.delete(sck) - sockats.delete(sck) + end + loop do + ready, = IO.select(sockets) + newtext = '' + newcolors = [] + + ready.each do |sck| + if sck == server + client = server.accept + sockats[client] = { + :color => { + :co8 => 7 + } + } + sockets << client + puts "Connection: #{client.peeraddr}" + client.puts "#{VERSION[:SRV_GSZ]}|#{ROWS}|#{COLS}" + client.puts new_buf_cob_wi_out(buffer,cobuf,0,buffer,cobuf)[3] + if buffer.length > 1 + client.puts new_buf_cob_wi_out( + buffer,cobuf,windex-1, + buffer[windex-1],[cobuf[windex-1]])[3] + end next end - ver, msg = msg.chomp.split('|',2) - next unless msg != nil - case ver - when VERSION[:CLN_TXT] - newtext << clean_chars(msg) - msg.length.times do - newcolors << sockats[sck][:color].dup - end - when VERSION[:CLN_ARO] - dir = 'l' - case msg - when 'A' - dir = 'u' - when 'B' - dir = 'd' - when 'C' - dir = 'r' - when 'D' - dir = 'l' - else + begin + msg = sck.gets + unless msg + puts "Disconnect: #{sck.peeraddr}" + sck.close + sockets.delete(sck) + sockats.delete(sck) next end - motions << { - :nti => newtext.length - 1, - :dir => dir, - } - when VERSION[:CLN_CO8] - sockats[sck][:color][:co8] = msg.to_i.clamp(0,7) - end - rescue Errno::ECONNRESET, Errno::EPIPE - puts "Disconnect (err): #{sck.peeraddr}" - sck.close - sockets.delete(sck) - end - buffer, cobuf, windex, out = new_buf_cob_wi_out(buffer,cobuf,windex,newtext,newcolors,motions:motions) - motions = [] - clients = sockets - [server] - clients.each do |c| - begin - c.puts out + ver, msg = msg.chomp.split('|',2) + next unless msg != nil + case ver + when VERSION[:CLN_TXT] + newtext << clean_chars(msg) + msg.length.times do + newcolors << sockats[sck][:color].dup + end + when VERSION[:CLN_ARO] + dir = 'l' + case msg + when 'A' + dir = 'u' + when 'B' + dir = 'd' + when 'C' + dir = 'r' + when 'D' + dir = 'l' + else + next + end + motions << { + :nti => newtext.length - 1, + :dir => dir, + } + when VERSION[:CLN_CO8] + sockats[sck][:color][:co8] = msg.to_i.clamp(0,7) + end rescue Errno::ECONNRESET, Errno::EPIPE - puts "Disconnect (err): #{c.peeraddr}" - c.close - sockets.delete(c) + puts "Disconnect (err): #{sck.peeraddr}" + sck.close + sockets.delete(sck) end + buffer, cobuf, windex, out = new_buf_cob_wi_out(buffer,cobuf,windex,newtext,newcolors,motions:motions) + motions = [] + clients = sockets - [server] + clients.each do |c| + begin + c.puts out + rescue Errno::ECONNRESET, Errno::EPIPE + puts "Disconnect (err): #{c.peeraddr}" + c.close + sockets.delete(c) + end + end + #if out != '' + # puts "#{windex}: (#{(windex/COLS)+1},#{(windex%COLS)+1})" + #end end - #if out != '' - # puts "#{windex}: (#{(windex/COLS)+1},#{(windex%COLS)+1})" - #end end + ensure + write_current(windex, buffer, cobuf, file_opts) + puts "File saved on exit." end end @@ -256,12 +376,61 @@ def clean_chars(chars) out end + def do_client socket = TCPSocket.new(HOST, PORT) + grid_rows = 24 + grid_cols = 40 puts "PORT: #{PORT}" puts "HOST: #{HOST}" + def print_mode(mode,gridrows) + return if ENV["TERM"].downcase == "dumb" + #print ("\x1b[s"+ # Save cursor + # "\x1b[m"+ # Reset color + # "\x1b[#{gridrows+1};1H"+# Cursor pos + # "\x1b[2K"+ # Clear line + # mode+ # Print mode + # "\x1b[u") # Restore cursor + print "\e[s\e[m\e[#{gridrows+1};1H\e[2K#{mode}\e[u" + end + + def parse_esckey(gridrows) + begin + char2 = STDIN.read_nonblock(1) + return [:RAW, "\e#{char2}"] unless char2 == '[' + rescue + return [:KEY_ESCAPE, ''] + end + begin + char3 = STDIN.read_nonblock(1) + rescue + return [:RAW, "\e#{char2}"] + end + case char3 + when 'A' + return [:ARROW, 'A'] + when 'B' + return [:ARROW, 'B'] + when 'C' + return [:ARROW, 'C'] + when 'D' + return [:ARROW, 'D'] + else + loop do + begin + char3 << STDIN.read_nonblock(1) + break unless char3[-1] =~ /[0-9:;\<=\>\?]/ + rescue + break + end + end + print_mode "#{clean_chars("\e"+char2+char3)} -- I'm unfamiliar with that key.",gridrows + return [:RAW, "\e#{char2}#{char3}"] + end + end + begin print "\x1b[?1049h\x1b[2J\x1b[3J" # Open and clear alternate buffer print "\x1b[m" # Reset color @@ -280,12 +449,16 @@ def do_client when VERSION[:SRV_CO8] next if ENV["TERM"].downcase == "dumb" print "\x1b[3#{msg.to_i.clamp(0,7)}m" + when VERSION[:SRV_GSZ] + grid_rows, grid_cols = msg.split('|',2).map { |p| p.to_i } + print_mode ":3",grid_rows end end end socket.puts "#{VERSION[:CLN_CO8]}|#{rand(1..7)}" while (char = STDIN.noecho(&:getch)) + print_mode ":3",grid_rows exit if char == "\x04" # ^D if char == "\x0F" # (^O) Color change if /[0-7]/.match? (char2 = STDIN.noecho(&:getch).upcase) @@ -295,12 +468,12 @@ def do_client char << char2 end elsif char == "\x1b" # Detect arrows - char2 = STDIN.noecho(&:getch) - if /[A-D]/.match? (char3 = STDIN.noecho(&:getch).upcase) - socket.puts "#{VERSION[:CLN_ARO]}|#{char3}" + keytype, keytext = parse_esckey(grid_rows) + if keytype == :ARROW + socket.puts "#{VERSION[:CLN_ARO]}|#{keytext}" next - else - char << char2+char3 + elsif keytype != :KEY_ESCAPE + char = keytext end elsif (char == "\x08" || # backspace char == "\x7F" ) # delete @@ -318,7 +491,7 @@ def do_client end if options[:server] - do_server + do_server(options[:file]) else do_client end