diff --git a/cmd/cmd.go b/cmd/cmd.go index 1d619dd05..a1e6bed68 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1 +1,54 @@ -package cmd +package main + +import ( + "fmt" + "go_dreamfactory/cmd/robot" + "os" + + flag "github.com/spf13/pflag" + + "github.com/spf13/cobra" +) + +func emptyRun(*cobra.Command, []string) {} + +var RootCmd = &cobra.Command{ + Use: "robot", + Short: "命令行", + Long: "命令行工具", + Run: emptyRun, +} + +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stdin, err) + os.Exit(1) + } +} + +func init() { + RootCmd.AddCommand(runCmd) +} + +var account = flag.String("account", "", "account") +var create = flag.Bool("create", false, "account") //false 不创建新账号 + +func main() { + Execute() +} + +var runCmd = &cobra.Command{ + Use: "run", + Short: "启动", + + Run: func(cmd *cobra.Command, args []string) { + opts := robot.DefaultOpts() + opts.Create = *create + if *create { + opts.Account = *account + } + + r := robot.NewRobot(opts) + r.Run() + }, +} diff --git a/cmd/robot/login.go b/cmd/robot/login.go new file mode 100644 index 000000000..654e4361a --- /dev/null +++ b/cmd/robot/login.go @@ -0,0 +1,38 @@ +package robot + +import ( + "fmt" + "go_dreamfactory/comm" + "go_dreamfactory/pb" + "log" + + "github.com/golang/protobuf/proto" +) + +func (r *Robot) handleLogin(methodName string) { + switch methodName { + case "login": + handleLogin(r) + default: + log.Fatal("methodName no exist") + } +} + +func handleLogin(r *Robot) { + loginreq := &pb.UserLoginReq{ + Name: "aaa", + } + logindata, _ := proto.Marshal(loginreq) + head := &pb.UserMessage{ + ServiceMethod: "login.login", + Data: logindata, + } + + if comm.ProtoEncode(loginreq, head) { + err := r.SendToClient(head.Data) + if err != nil { + fmt.Printf("err:%v\n", err) + } + } + +} diff --git a/cmd/robot/options.go b/cmd/robot/options.go new file mode 100644 index 000000000..827bdbc3a --- /dev/null +++ b/cmd/robot/options.go @@ -0,0 +1,36 @@ +package robot + +type Options struct { + WsUrl string //客户端访问网关的ws接口地址 + RegUrl string //账号注册接口地址 + Account string //玩家账号 + Create bool +} + +func DefaultOpts() *Options { + return &Options{ + WsUrl: "ws://localhost:7891/gateway", + RegUrl: "http://localhost:8000/register", + Create: false, + } +} + +type Option func(*Options) + +func WithWsUrl(addr string) Option { + return func(o *Options) { + o.WsUrl = addr + } +} + +func WithAccount(account string) Option { + return func(o *Options) { + o.Account = account + } +} + +func WithCreate(create bool) Option { + return func(o *Options) { + o.Create = create + } +} diff --git a/cmd/robot/robot.go b/cmd/robot/robot.go index 592b686d1..5ca3a97c9 100644 --- a/cmd/robot/robot.go +++ b/cmd/robot/robot.go @@ -1,2 +1,135 @@ package robot +import ( + "bytes" + "encoding/json" + "go_dreamfactory/comm" + "go_dreamfactory/pb" + "go_dreamfactory/utils" + "io/ioutil" + "log" + "net/http" + + "github.com/golang/protobuf/proto" + "github.com/gorilla/websocket" + jsoniter "github.com/json-iterator/go" +) + +type Robot struct { + ws *websocket.Conn + Opts *Options +} + +func NewRobot(opts *Options) *Robot { + ws, _, err := websocket.DefaultDialer.Dial(opts.WsUrl, nil) + if err != nil { + log.Fatal(err) + } + r := &Robot{ + ws: ws, + Opts: opts, + } + + return r +} + +func (r *Robot) Run() { + log.Print("Robot running...") + log.Printf("websocket %s \n", r.Opts.WsUrl) + + if r.Opts.Create { //创建新用户 + r.AccountRegister() + } else { + //login user + r.AccountLogin() + } + + for { + var msg *pb.UserMessage = &pb.UserMessage{} + _, data, err := r.ws.ReadMessage() + if err != nil { + log.Fatal(err) + } + + if err = proto.Unmarshal(data, msg); err != nil { + log.Fatal(err) + } + r.handleMsg(msg) + } +} + +func (r *Robot) handleMsg(msg *pb.UserMessage) { + m, f, ok := utils.ParseP(msg.ServiceMethod) + if !ok { + log.Fatal("route error") + } + switch m { + case "login": + r.handleLogin(f) + default: + log.Fatal("module route no exist") + } +} + +func (r *Robot) SendToClient(data []byte) error { + return r.ws.WriteMessage(websocket.BinaryMessage, data) +} + +//注册账号 +func (r *Robot) AccountRegister() { + //http + regReq := &pb.UserRegisterReq{} + jsonByte, _ := json.Marshal(regReq) + req, err := http.NewRequest("POST", r.Opts.RegUrl, bytes.NewReader(jsonByte)) + if err != nil { + log.Fatalf("account register err %v", err) + } + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + httpClient := &http.Client{} + rsp, err := httpClient.Do(req) + if err != nil { + panic(err) + } + defer rsp.Body.Close() + + body, _ := ioutil.ReadAll(rsp.Body) + regRsp := &pb.UserRegisterRsp{} + err = jsoniter.Unmarshal(body, regRsp) + + if regRsp.Code == comm.ErrorCode_Success { //注册成功 + //登录 + loginReg := &pb.UserLoginReq{ + Name: regReq.Account, + } + + head := &pb.UserMessage{ + ServiceMethod: "login.login", + } + if comm.ProtoEncode(loginReg, head) { + err = r.SendToClient(head.Data) + if err != nil { + log.Fatal(err) + } + } + + } + +} + +//登录认证 +func (r *Robot) AccountLogin() { + //登录 + loginReg := &pb.UserLoginReq{ + Name: r.Opts.Account, + } + + head := &pb.UserMessage{ + ServiceMethod: "login.login", + } + if comm.ProtoEncode(loginReg, head) { + err := r.SendToClient(head.Data) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/cmd/robot/user.go b/cmd/robot/user.go new file mode 100644 index 000000000..8e1a8faab --- /dev/null +++ b/cmd/robot/user.go @@ -0,0 +1 @@ +package robot diff --git a/comm/core.go b/comm/core.go index ccaa87978..504a6dc18 100644 --- a/comm/core.go +++ b/comm/core.go @@ -1,9 +1,11 @@ package comm import ( + "go_dreamfactory/pb" "reflect" "github.com/liwei1dao/lego/core" + "github.com/liwei1dao/lego/sys/log" "google.golang.org/protobuf/proto" ) @@ -54,3 +56,22 @@ type Message struct { Head *MessageHead Data []byte } + +func ProtoDecode(msg *pb.UserMessage, req proto.Message) (ok bool) { + err := proto.Unmarshal(msg.Data, req) + if err != nil { + log.Errorf("%s %v", msg.ServiceMethod, err) + return + } + return true +} + +func ProtoEncode(rsp proto.Message, msg *pb.UserMessage) (ok bool) { + data, err := proto.Marshal(rsp) + if err != nil { + log.Errorf("%s %v", msg.ServiceMethod, err) + return + } + msg.Data = data + return true +} diff --git a/go.mod b/go.mod index 486587b1f..daf1be6cd 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,13 @@ module go_dreamfactory go 1.18 require ( + github.com/golang/protobuf v1.5.2 github.com/gorilla/websocket v1.4.2 + github.com/json-iterator/go v1.1.12 github.com/liwei1dao/lego v0.0.0-20220531091126-a21bb3766fbc + github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.1 + go.mongodb.org/mongo-driver v1.5.1 google.golang.org/protobuf v1.28.0 ) @@ -51,8 +55,8 @@ require ( github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.9.7 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ratelimit v1.0.1 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/kavu/go_reuseport v1.5.0 // indirect @@ -91,6 +95,7 @@ require ( github.com/smallnest/quick v0.0.0-20220103065406-780def6371e6 // indirect github.com/smallnest/rpcx v1.7.4 // indirect github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect github.com/tinylib/msgp v1.1.6 // indirect @@ -106,7 +111,6 @@ require ( github.com/xdg-go/stringprep v1.0.2 // indirect github.com/xtaci/kcp-go v5.4.20+incompatible // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - go.mongodb.org/mongo-driver v1.5.1 // indirect go.opentelemetry.io/otel v1.6.3 // indirect go.opentelemetry.io/otel/trace v1.6.3 // indirect go.uber.org/atomic v1.7.0 // indirect diff --git a/go.sum b/go.sum index 0181ca61d..77febd27a 100644 --- a/go.sum +++ b/go.sum @@ -427,6 +427,7 @@ github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb-client-go/v2 v2.8.2/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk= github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= @@ -753,9 +754,11 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/modules/web/api_comp.go b/modules/web/api_comp.go index bd707c186..2193112d4 100644 --- a/modules/web/api_comp.go +++ b/modules/web/api_comp.go @@ -1,10 +1,16 @@ package web import ( + "go_dreamfactory/comm" + "go_dreamfactory/pb" + "go_dreamfactory/sys/db" + "net/http" + "github.com/liwei1dao/lego/core" "github.com/liwei1dao/lego/core/cbase" "github.com/liwei1dao/lego/sys/gin" "github.com/liwei1dao/lego/sys/gin/engine" + "github.com/liwei1dao/lego/sys/log" ) type Api_Comp struct { @@ -17,10 +23,26 @@ func (this *Api_Comp) Init(service core.IService, module core.IModule, comp core err = this.ModuleCompBase.Init(service, module, comp, options) this.options = options.(*Options) this.gin, err = gin.NewSys(gin.SetListenPort(this.options.Port)) - this.gin.GET("./test", this.test) + this.gin.POST("/register", this.Register) return } -func (this *Api_Comp) test(c *engine.Context) { - +//模拟账户注册 +func (this *Api_Comp) Register(c *engine.Context) { + var req pb.UserRegisterReq + rsp := &pb.UserRegisterRsp{} + err := c.BindJSON(&req) + if err == nil { + err := db.Defsys.CreateUser(&pb.DB_UserData{ + Account: req.Account, + }) + if err != nil { + log.Errorf("create user err: %v", err) + rsp.Code = comm.ErrorCode_SqlExecutionError + } + rsp.Code = comm.ErrorCode_Success + } else { + rsp.Code = comm.ErrorCode_ReqParameterError + } + c.JSON(http.StatusOK, rsp) } diff --git a/pb/proto/user_msg.proto b/pb/proto/user_msg.proto index 1aec4e531..fad4a93b6 100644 --- a/pb/proto/user_msg.proto +++ b/pb/proto/user_msg.proto @@ -8,3 +8,12 @@ message UserLoginReq { message UserLoginResp { int32 Code = 1; } + + +message UserRegisterReq{ + string account = 1; +} + +message UserRegisterRsp{ + int32 Code = 1; +} \ No newline at end of file diff --git a/pb/user_msg.pb.go b/pb/user_msg.pb.go index 77e09793f..69042d7a9 100644 --- a/pb/user_msg.pb.go +++ b/pb/user_msg.pb.go @@ -114,6 +114,100 @@ func (x *UserLoginResp) GetCode() int32 { return 0 } +type UserRegisterReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` +} + +func (x *UserRegisterReq) Reset() { + *x = UserRegisterReq{} + if protoimpl.UnsafeEnabled { + mi := &file_user_msg_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserRegisterReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRegisterReq) ProtoMessage() {} + +func (x *UserRegisterReq) ProtoReflect() protoreflect.Message { + mi := &file_user_msg_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRegisterReq.ProtoReflect.Descriptor instead. +func (*UserRegisterReq) Descriptor() ([]byte, []int) { + return file_user_msg_proto_rawDescGZIP(), []int{2} +} + +func (x *UserRegisterReq) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +type UserRegisterRsp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=Code,proto3" json:"Code,omitempty"` +} + +func (x *UserRegisterRsp) Reset() { + *x = UserRegisterRsp{} + if protoimpl.UnsafeEnabled { + mi := &file_user_msg_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserRegisterRsp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRegisterRsp) ProtoMessage() {} + +func (x *UserRegisterRsp) ProtoReflect() protoreflect.Message { + mi := &file_user_msg_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRegisterRsp.ProtoReflect.Descriptor instead. +func (*UserRegisterRsp) Descriptor() ([]byte, []int) { + return file_user_msg_proto_rawDescGZIP(), []int{3} +} + +func (x *UserRegisterRsp) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + var File_user_msg_proto protoreflect.FileDescriptor var file_user_msg_proto_rawDesc = []byte{ @@ -122,8 +216,13 @@ var file_user_msg_proto_rawDesc = []byte{ 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x23, 0x0a, 0x0d, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x3b, 0x70, - 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x01, 0x28, 0x05, 0x52, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a, 0x07, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x25, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x73, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x43, 0x6f, 0x64, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x42, 0x06, 0x5a, + 0x04, 0x2e, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -138,10 +237,12 @@ func file_user_msg_proto_rawDescGZIP() []byte { return file_user_msg_proto_rawDescData } -var file_user_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_user_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_user_msg_proto_goTypes = []interface{}{ - (*UserLoginReq)(nil), // 0: UserLoginReq - (*UserLoginResp)(nil), // 1: UserLoginResp + (*UserLoginReq)(nil), // 0: UserLoginReq + (*UserLoginResp)(nil), // 1: UserLoginResp + (*UserRegisterReq)(nil), // 2: UserRegisterReq + (*UserRegisterRsp)(nil), // 3: UserRegisterRsp } var file_user_msg_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -181,6 +282,30 @@ func file_user_msg_proto_init() { return nil } } + file_user_msg_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserRegisterReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_user_msg_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserRegisterRsp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -188,7 +313,7 @@ func file_user_msg_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_user_msg_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/services/web/main.go b/services/web/main.go index 03e73da3b..43e73f3b3 100644 --- a/services/web/main.go +++ b/services/web/main.go @@ -2,12 +2,16 @@ package main import ( "flag" + "fmt" "go_dreamfactory/modules/web" "go_dreamfactory/services" + "go_dreamfactory/sys/cache" + "go_dreamfactory/sys/db" "github.com/liwei1dao/lego" "github.com/liwei1dao/lego/base/rpcx" "github.com/liwei1dao/lego/core" + "github.com/liwei1dao/lego/sys/log" ) var ( @@ -41,4 +45,14 @@ type Service struct { func (this *Service) InitSys() { this.ServiceBase.InitSys() + if err := cache.OnInit(this.GetSettings().Sys["cache"]); err != nil { + panic(fmt.Sprintf("init sys.cache err: %s", err.Error())) + } else { + log.Infof("init sys.cache success!") + } + if err := db.OnInit(this.GetSettings().Sys["db"]); err != nil { + panic(fmt.Sprintf("init sys.db err: %s", err.Error())) + } else { + log.Infof("init sys.db success!") + } } diff --git a/utils/strings.go b/utils/strings.go new file mode 100644 index 000000000..883d73472 --- /dev/null +++ b/utils/strings.go @@ -0,0 +1,17 @@ +package utils + +import ( + "strings" + + "github.com/liwei1dao/lego/sys/log" +) + +func ParseP(p string) (string, string, bool) { + s := strings.SplitN(p, ".", 2) + if len(s) < 2 { + log.Debugf("param P err: %s", p) + return "", "", false + } + + return s[0], s[1], true +}