]> git.lizzy.rs Git - mt.git/commitdiff
Add inv (de)serialization
authoranon5 <anon5clam@protonmail.com>
Mon, 22 Feb 2021 21:09:59 +0000 (21:09 +0000)
committeranon5 <anon5clam@protonmail.com>
Mon, 22 Feb 2021 21:09:59 +0000 (21:09 +0000)
inv.go [new file with mode: 0644]
readrune.go [new file with mode: 0644]
stack.go [new file with mode: 0644]

diff --git a/inv.go b/inv.go
new file mode 100644 (file)
index 0000000..5113ee3
--- /dev/null
+++ b/inv.go
@@ -0,0 +1,192 @@
+package mt
+
+import (
+       "fmt"
+       "io"
+       "reflect"
+)
+
+type sentinal struct {
+       err error
+}
+
+func (s *sentinal) ret(err error) {
+       s.err = err
+       panic(s)
+}
+
+type Inv []NamedInvList
+
+type NamedInvList struct {
+       Name string
+       InvList
+}
+
+func (inv Inv) List(name string) *NamedInvList {
+       for i, l := range inv {
+               if l.Name == name {
+                       return &inv[i]
+               }
+       }
+       return nil
+}
+
+func (i Inv) Serialize(w io.Writer) error {
+       for _, l := range i {
+               if _, err := fmt.Fprintln(w, "List", l.Name, len(l.Stacks)); err != nil {
+                       return err
+               }
+               if err := l.Serialize(w); err != nil {
+                       return err
+               }
+       }
+       _, err := fmt.Fprintln(w, "EndInventory")
+       return err
+}
+
+func (i *Inv) Deserialize(r io.Reader) (err error) {
+       s := new(sentinal)
+       defer func() {
+               r := recover()
+               if r, ok := r.(sentinal); ok {
+                       err = r.err
+               }
+       }()
+
+       old := *i
+       *i = nil
+
+       for {
+               if err := readCmdLn(r, map[string]interface{}{
+                       "List": func(name string, size int) {
+                               l := old.List(name)
+                               if l == nil {
+                                       l = &NamedInvList{Name: name}
+                               }
+
+                               if err := l.Deserialize(r); err != nil {
+                                       s.ret(fmt.Errorf("List %s %d: %w", name, size, err))
+                               }
+                               if len(l.Stacks) != size {
+                                       s.ret(fmt.Errorf("List %s %d: contains %d stacks", name, size, len(l.Stacks)))
+                               }
+
+                               *i = append(*i, *l)
+                       },
+                       "KeepList": func(name string) {
+                               l := old.List(name)
+                               if l == nil {
+                                       s.ret(fmt.Errorf("KeepList %s: list does not exist", name))
+                               }
+
+                               *i = append(*i, *l)
+                       },
+                       "EndInventory": func() {
+                               s.ret(nil)
+                       },
+               }); err != nil {
+                       if err == io.EOF {
+                               s.ret(io.ErrUnexpectedEOF)
+                       }
+                       s.ret(err)
+               }
+       }
+}
+
+type InvList struct {
+       Width  int
+       Stacks []Stack
+}
+
+func (l InvList) Serialize(w io.Writer) error {
+       if _, err := fmt.Fprintln(w, "Width", l.Width); err != nil {
+               return err
+       }
+       for _, i := range l.Stacks {
+               if i.Count > 0 {
+                       if _, err := fmt.Fprintln(w, "Item", i); err != nil {
+                               return err
+                       }
+               } else {
+                       if _, err := fmt.Fprintln(w, "Empty"); err != nil {
+                               return err
+                       }
+               }
+       }
+       _, err := fmt.Fprintln(w, "EndInventoryList")
+       return err
+}
+
+func (i *InvList) Deserialize(r io.Reader) (err error) {
+       s := new(sentinal)
+       defer func() {
+               r := recover()
+               if r, ok := r.(sentinal); ok {
+                       err = r.err
+               }
+       }()
+
+       if _, err := fmt.Fscanf(r, "Width %d\n", &i.Width); err != nil {
+               s.ret(err)
+       }
+
+       i.Stacks = i.Stacks[:0]
+
+       for {
+               if err := readCmdLn(r, map[string]interface{}{
+                       "Empty": func() {
+                               i.Stacks = append(i.Stacks, Stack{})
+                       },
+                       "Item": func(stk Stack) {
+                               i.Stacks = append(i.Stacks, stk)
+                       },
+                       "Keep": func() {
+                               if len(i.Stacks) < cap(i.Stacks) {
+                                       i.Stacks = i.Stacks[:len(i.Stacks)+1]
+                               } else {
+                                       i.Stacks = append(i.Stacks, Stack{})
+                               }
+                       },
+                       "EndInventoryList": func() {
+                               s.ret(nil)
+                       },
+               }); err != nil {
+                       if err == io.EOF {
+                               s.ret(io.ErrUnexpectedEOF)
+                       }
+                       s.ret(err)
+               }
+       }
+}
+
+func readCmdLn(r io.Reader, cmds map[string]interface{}) error {
+       if _, ok := r.(io.RuneScanner); !ok {
+               r = &readRune{Reader: r, peekRune: -1}
+       }
+
+       var cmd string
+       if _, err := fmt.Fscan(r, &cmd); err != nil {
+               return err
+       }
+
+       f, ok := cmds[cmd]
+       if !ok {
+               return fmt.Errorf("unsupported line type: %+q", cmd)
+       }
+
+       t := reflect.TypeOf(f)
+
+       a := make([]interface{}, t.NumIn())
+       for i := range a {
+               a[i] = reflect.New(t.In(i)).Interface()
+       }
+       fmt.Fscanln(r, a...)
+
+       args := make([]reflect.Value, t.NumIn())
+       for i := range args {
+               args[i] = reflect.ValueOf(a[i]).Elem()
+       }
+       reflect.ValueOf(f).Call(args)
+
+       return nil
+}
diff --git a/readrune.go b/readrune.go
new file mode 100644 (file)
index 0000000..775e06f
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+Based on go1.16/src/fmt/scan.go.
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package mt
+
+import (
+       "errors"
+       "io"
+       "unicode/utf8"
+)
+
+// readRune is a structure to enable reading UTF-8 encoded code points
+// from an io.Reader. It is used if the Reader given to the scanner does
+// not already implement io.RuneScanner.
+type readRune struct {
+       io.Reader
+       buf      [utf8.UTFMax]byte // used only inside ReadRune
+       pending  int               // number of bytes in pendBuf; only >0 for bad UTF-8
+       pendBuf  [utf8.UTFMax]byte // bytes left over
+       peekRune rune              // if >=0 next rune; when <0 is ^(previous Rune)
+}
+
+// readByte returns the next byte from the input, which may be
+// left over from a previous read if the UTF-8 was ill-formed.
+func (r *readRune) readByte() (b byte, err error) {
+       if r.pending > 0 {
+               b = r.pendBuf[0]
+               copy(r.pendBuf[0:], r.pendBuf[1:])
+               r.pending--
+               return
+       }
+       n, err := io.ReadFull(r, r.pendBuf[:1])
+       if n != 1 {
+               return 0, err
+       }
+       return r.pendBuf[0], err
+}
+
+// ReadRune returns the next UTF-8 encoded code point from the
+// io.Reader inside r.
+func (r *readRune) ReadRune() (rr rune, size int, err error) {
+       if r.peekRune >= 0 {
+               rr = r.peekRune
+               r.peekRune = ^r.peekRune
+               size = utf8.RuneLen(rr)
+               return
+       }
+       r.buf[0], err = r.readByte()
+       if err != nil {
+               return
+       }
+       if r.buf[0] < utf8.RuneSelf { // fast check for common ASCII case
+               rr = rune(r.buf[0])
+               size = 1 // Known to be 1.
+               // Flip the bits of the rune so it's available to UnreadRune.
+               r.peekRune = ^rr
+               return
+       }
+       var n int
+       for n = 1; !utf8.FullRune(r.buf[:n]); n++ {
+               r.buf[n], err = r.readByte()
+               if err != nil {
+                       if err == io.EOF {
+                               err = nil
+                               break
+                       }
+                       return
+               }
+       }
+       rr, size = utf8.DecodeRune(r.buf[:n])
+       if size < n { // an error, save the bytes for the next read
+               copy(r.pendBuf[r.pending:], r.buf[size:n])
+               r.pending += n - size
+       }
+       // Flip the bits of the rune so it's available to UnreadRune.
+       r.peekRune = ^rr
+       return
+}
+
+func (r *readRune) UnreadRune() error {
+       if r.peekRune >= 0 {
+               return errors.New("fmt: scanning called UnreadRune with no rune available")
+       }
+       // Reverse bit flip of previously read rune to obtain valid >=0 state.
+       r.peekRune = ^r.peekRune
+       return nil
+}
diff --git a/stack.go b/stack.go
new file mode 100644 (file)
index 0000000..fa83d8d
--- /dev/null
+++ b/stack.go
@@ -0,0 +1,232 @@
+// In this file, JSON refers to WTF-JSON, a variant of JSON used by Minetest
+// where \u00XX escapes in string literals act like Go's \xXX escapes.
+
+package mt
+
+import (
+       "fmt"
+       "io"
+       "strconv"
+       "strings"
+       "unicode/utf8"
+)
+
+type Stack struct {
+       Item
+       Count uint16
+}
+
+type Item struct {
+       Name string
+       Wear uint16
+       ItemMeta
+}
+
+type ItemMeta string
+
+func (m ItemMeta) Field(name string) (s string, ok bool) {
+       if len(m) > 0 && m[0] == 1 {
+               m = m[1:]
+               eat := func(stop byte) string {
+                       for i := 0; i < len(m); i++ {
+                               if m[i] == stop {
+                                       defer func() {
+                                               m = m[i+1:]
+                                       }()
+                                       return string(m[:i])
+                               }
+                       }
+                       defer func() {
+                               m = ""
+                       }()
+                       return string(m)
+               }
+               for len(m) > 0 {
+                       if eat(2) == name {
+                               s = eat(3)
+                               ok = true
+                       }
+               }
+               return
+       }
+
+       if name == "" {
+               return string(m), true
+       }
+
+       return "", false
+}
+
+func (s Stack) String() string {
+       if s.Count == 0 {
+               return ""
+       }
+
+       n := 1
+       if s.ItemMeta != "" {
+               n = 4
+       } else if s.Wear > 0 {
+               n = 3
+       } else if s.Count > 1 {
+               n = 2
+       }
+
+       return strings.Join([]string{
+               optJSONStr(s.Name),
+               fmt.Sprint(s.Count),
+               fmt.Sprint(s.Wear),
+               optJSONStr(string(s.ItemMeta)),
+       }[:n], " ")
+}
+
+func optJSONStr(s string) string {
+       for _, r := range s {
+               if r <= ' ' || r == '"' || r >= utf8.RuneSelf {
+                       return jsonStr(s)
+               }
+       }
+       return s
+}
+
+func jsonStr(s string) string {
+       esc := [256]byte{
+               '\\': '\\',
+               '"':  '"',
+               '/':  '/',
+               '\b': 'b',
+               '\f': 'f',
+               '\n': 'n',
+               '\r': 'r',
+               '\t': 't',
+       }
+
+       b := new(strings.Builder)
+
+       b.WriteByte('"')
+       for i := 0; i < len(s); i++ {
+               switch c := s[i]; {
+               case esc[c] != 0:
+                       fmt.Fprintf(b, "\\%c", esc[c])
+               case ' ' <= c && c <= '~':
+                       b.WriteByte(c)
+               default:
+                       fmt.Fprintf(b, "\\u%04x", c)
+               }
+       }
+       b.WriteByte('"')
+
+       return b.String()
+}
+
+func (stk *Stack) Scan(state fmt.ScanState, verb rune) (err error) {
+       *stk = Stack{}
+
+       defer func() {
+               if err == io.EOF {
+                       err = nil
+               }
+       }()
+
+       nm, err := scanOptJSONStr(state)
+       if err != nil {
+               return err
+       }
+       stk.Name = nm
+       stk.Count = 1
+
+       if _, err := fmt.Fscan(state, &stk.Count, &stk.Wear); err != nil {
+               return err
+       }
+
+       s, err := scanOptJSONStr(state)
+       if err != nil {
+               return err
+       }
+       stk.ItemMeta = ItemMeta(s)
+
+       return nil
+}
+
+func scanOptJSONStr(state fmt.ScanState) (string, error) {
+       state.SkipSpace()
+
+       r, _, err := state.ReadRune()
+       if err != nil {
+               return "", err
+       }
+       state.UnreadRune()
+
+       if r == '"' {
+               return scanJSONStr(state)
+       }
+
+       token, err := state.Token(false, func(r rune) bool { return r != ' ' })
+       return string(token), err
+}
+
+func scanJSONStr(state fmt.ScanState) (s string, rerr error) {
+       r, _, err := state.ReadRune()
+       if err != nil {
+               return "", err
+       }
+       if r != '"' {
+               return "", fmt.Errorf("unexpected rune: %q", r)
+       }
+
+       defer func() {
+               if rerr == io.EOF {
+                       rerr = io.ErrUnexpectedEOF
+               }
+       }()
+
+       b := new(strings.Builder)
+       for {
+               r, _, err := state.ReadRune()
+               if err != nil {
+                       return b.String(), err
+               }
+
+               switch r {
+               case '"':
+                       return b.String(), nil
+               case '\\':
+                       r, _, err := state.ReadRune()
+                       if err != nil {
+                               return b.String(), err
+                       }
+
+                       switch r {
+                       case '\\', '"', '/':
+                               b.WriteRune(r)
+                       case 'b':
+                               b.WriteRune('\b')
+                       case 'f':
+                               b.WriteRune('\f')
+                       case 'n':
+                               b.WriteRune('\n')
+                       case 'r':
+                               b.WriteRune('\r')
+                       case 't':
+                               b.WriteRune('\t')
+                       case 'u':
+                               var hex [4]rune
+                               for i := range hex {
+                                       r, _, err := state.ReadRune()
+                                       if err != nil {
+                                               return b.String(), err
+                                       }
+                                       hex[i] = r
+                               }
+                               n, err := strconv.ParseUint(string(hex[:]), 16, 8)
+                               if err != nil {
+                                       return b.String(), err
+                               }
+                               b.WriteByte(byte(n))
+                       default:
+                               return b.String(), fmt.Errorf("invalid escape: \\%c", r)
+                       }
+               default:
+                       b.WriteRune(r)
+               }
+       }
+}