├── .gitignore ├── Editor.meta ├── Editor ├── commds.txt ├── commds.txt.meta ├── rshell.py ├── rshell.py.meta ├── rshell_install.bat ├── rshell_install.bat.meta ├── rshell_local.bat ├── rshell_local.bat.meta ├── rshell_remote.bat └── rshell_remote.bat.meta ├── README.md ├── README.md.meta ├── Runtime.meta └── Runtime ├── CmdHelper.meta ├── CmdHelper ├── Dumper.cs ├── Dumper.cs.meta ├── Operator.cs ├── Operator.cs.meta ├── Scene.cs ├── Scene.cs.meta ├── TestEvaluator.cs └── TestEvaluator.cs.meta ├── FunctionEvaluator.cs ├── FunctionEvaluator.cs.meta ├── Shell.Test.cs ├── Shell.Test.cs.meta ├── Shell.cs ├── Shell.cs.meta ├── UdpHost.cs └── UdpHost.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Recordings can get excessive in size 18 | /[Rr]ecordings/ 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | *.app 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # Packed Addressables 68 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 69 | 70 | # Temporary auto-generated Android Assets 71 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 72 | /[Aa]ssets/[Ss]treamingAssets/aa/* 73 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7902b89ccedf5844b3bf24c9888ac80 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | supportLOD: 0 7 | lOD: 0 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Editor/commds.txt: -------------------------------------------------------------------------------- 1 | help // 打开帮助 2 | Shell.TestSelf() // RShell解释器自测 3 | Shell.LoadLibrary("libhook.so") // 利用RShell进行注入 4 | Dumper.Do // 反射打印任意对象 5 | Screen.SetResolution(500, 300, true) // 设置分辨率 6 | Application.targetFrameRate // 帧率 7 | Application.identifier // 包名 8 | SystemInfo.deviceModel // 查询设备型号 9 | -------------------------------------------------------------------------------- /Editor/commds.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b0ab7a3daf38a5a4f9700903fb028ef6 3 | TextScriptImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/rshell.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import sys 4 | import os 5 | import time 6 | import json 7 | from enum import Enum 8 | 9 | debug_rshell = False 10 | class ConnectState(Enum): 11 | Default = 1, 12 | Connecting = 2, 13 | Connected = 3, 14 | 15 | def print_debug(msg): 16 | if debug_rshell: 17 | print(msg) 18 | 19 | def receive_message(shell): 20 | while True: 21 | try: 22 | if(shell.client_socket is None): 23 | break 24 | data, addr = shell.client_socket.recvfrom(1024 * 1024) 25 | msg = json.loads(data.decode('utf-8')) 26 | shell.connect_state = ConnectState.Connected 27 | shell.add_to_buffer(msg) 28 | except Exception as e: 29 | if debug_rshell: 30 | print("Error occurred while receiving message:", str(e)) 31 | shell.connect_state = ConnectState.Default 32 | break 33 | 34 | class RShell(object): 35 | 36 | def add_to_buffer(self, msg): 37 | buffer = [] 38 | msgid = msg['MsgId'] 39 | 40 | if msgid not in self.msgbuffer: 41 | self.msgbuffer[msgid] = buffer 42 | else: 43 | buffer = self.msgbuffer[msgid] 44 | 45 | buffer.append(msg) 46 | 47 | if len(buffer) == msg['FragCount']: 48 | del self.msgbuffer[msgid] 49 | buffer.sort(key=lambda x: x['FragIndex']) 50 | content = "".join([msg['Content'] for msg in buffer]) 51 | 52 | if content != "welcome": 53 | sendmsg = msg["CheckMsg"] if "CheckMsg" in msg else None 54 | self._add_respond_msg(sendmsg, content) 55 | 56 | if self.on_message_received is not None: 57 | self.on_message_received(content) 58 | else: 59 | print_debug("transporting {0} of {1} fragments ... ".format(len(buffer), msg['FragCount'])) 60 | pass 61 | 62 | 63 | def __init__(self, address = None): 64 | self.target_ip = "127.0.0.1" 65 | self.target_port = 9999 66 | self.on_message_received = None 67 | self.connect_state = 0 68 | self.client_socket = None 69 | 70 | if address is not None: 71 | args = address.split(':') 72 | self.target_ip = args[0] 73 | if len(args) > 1: 74 | self.target_port = int(args[1]) 75 | 76 | self.init_socket() 77 | pass 78 | 79 | def close_socket(self): 80 | if self.client_socket is not None: 81 | self.client_socket.close() 82 | self.client_socket = None 83 | pass 84 | 85 | def init_socket(self, reconnect = False): 86 | if self.connect_state != ConnectState.Connecting: 87 | reconnectinfo = "(reconnect)" if reconnect else "" 88 | print(f"RShell is connecting ... {self.target_ip}:{self.target_port} {reconnectinfo}") 89 | self.connect_state = ConnectState.Connecting 90 | 91 | self._reset_respond_msg(None) 92 | self.msgbuffer = {} 93 | 94 | self.close_socket() 95 | self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 96 | self.client_socket.sendto("hi".encode('utf-8'), (self.target_ip, self.target_port)) 97 | 98 | self.receive_thread = threading.Thread(target=receive_message, name='rshell', args=(self,)) 99 | self.receive_thread.start() 100 | pass 101 | 102 | def send(self, message): 103 | if self.connect_state != ConnectState.Connected: 104 | self.init_socket() 105 | self._send(message) 106 | pass 107 | 108 | def sendwait(self, message, waittime = 10, retry = 10): 109 | self._reset_respond_msg(message) 110 | self._send(message) 111 | start_time = time.time() 112 | 113 | while True: 114 | respond = self._get_respond_msg(message) 115 | if respond is None: 116 | if time.time() - start_time > waittime: # timeout 117 | if retry == 0: 118 | raise Exception("Timeout while waiting for response.") 119 | print_debug("Timeout while waiting for response. try again.") 120 | self.init_socket(True) 121 | return self.sendwait(message, waittime, retry - 1) 122 | else: # wait 123 | time.sleep(0.1) 124 | else: 125 | return respond 126 | 127 | def sendwait_print(self, message, waittime = 10, retry = 10): 128 | msg = self.sendwait(message, waittime, retry) 129 | print(f"rshell send: {message} -> respond:{msg}") 130 | return msg 131 | 132 | def _send(self, message): 133 | try: 134 | self.client_socket.sendto(message.encode('utf-8'), (self.target_ip, self.target_port)) 135 | except Exception as e: 136 | print_debug("Error occurred while sending message:", str(e)) 137 | pass 138 | 139 | def _reset_respond_msg(self, sendmsg): 140 | self.respond_msg = None 141 | 142 | if sendmsg: 143 | self.respond_msg_dic[sendmsg] = None 144 | else: 145 | self.respond_msg_dic = {} 146 | pass 147 | 148 | def _get_respond_msg(self, sendmsg): 149 | if self.respond_msg is not None: 150 | return self.respond_msg 151 | return self.respond_msg_dic[sendmsg] if sendmsg in self.respond_msg_dic else None 152 | 153 | def _add_respond_msg(self, sendmsg, respond): 154 | if sendmsg: 155 | self.respond_msg_dic[sendmsg] = respond 156 | else: 157 | self.respond_msg = respond 158 | pass 159 | 160 | 161 | # rshell interactive mode 162 | if __name__ == '__main__': 163 | import subprocess 164 | import pyfiglet 165 | from prompt_toolkit import PromptSession 166 | from prompt_toolkit.completion import Completer, Completion 167 | 168 | commands_with_descriptions = None 169 | class CustomCompleter(Completer): 170 | def get_completions(self, document, complete_event): 171 | word_before_cursor = document.get_word_before_cursor().strip().lower() 172 | if commands_with_descriptions is None: 173 | return 174 | for cmd, description in commands_with_descriptions.items(): 175 | if word_before_cursor in cmd.lower() or word_before_cursor in description.lower(): 176 | yield Completion(cmd, start_position=-len(word_before_cursor), display=cmd, display_meta=description) 177 | 178 | def read_commands(path): 179 | cmds = {} 180 | if not os.path.exists(path): 181 | return cmds 182 | 183 | with open(path, "r", encoding="utf-8") as f: 184 | for line in f: 185 | line_striped = line.strip() 186 | if line_striped == "": 187 | continue 188 | 189 | if "//" in line_striped: 190 | cmd, description = line_striped.split("//") 191 | cmds[cmd] = description 192 | else: 193 | cmds[line_striped] = "" 194 | pass 195 | pass 196 | pass 197 | return cmds 198 | 199 | os.chdir(sys.path[0]) 200 | 201 | commands_with_descriptions = read_commands("commds.txt") 202 | 203 | address = sys.argv[1] if len(sys.argv) > 1 else None 204 | debug_rshell = True 205 | 206 | rshell = RShell(address) 207 | 208 | # Generate ASCII art with a specific font 209 | ascii_art = pyfiglet.figlet_format("RShell", font="slant") 210 | session = PromptSession(completer=CustomCompleter()) 211 | 212 | # Print the ASCII art 213 | print(ascii_art) 214 | print("Type 'h' or 'help' to see available commands.\nControl + C to exit.\n") 215 | 216 | try: 217 | while True: 218 | message = session.prompt(f"{rshell.target_ip}:{rshell.target_port}>").strip() 219 | 220 | if message == 'help' or message == 'h': 221 | subprocess.Popen("commds.txt", shell=True) 222 | continue 223 | 224 | if message.endswith(".txt") and os.path.exists(message): 225 | rshell.on_message_received = None 226 | cmds = read_commands(message) 227 | for c, d in cmds.items(): 228 | print(f"// {d}") 229 | rshell.sendwait_print(c) 230 | else: 231 | rshell.on_message_received = lambda msg: print(msg) 232 | if message == "": 233 | message = "hi" 234 | rshell.send(message) 235 | pass 236 | time.sleep(0.1) 237 | pass 238 | except KeyboardInterrupt: 239 | print("Exiting ...") 240 | finally: 241 | rshell.close_socket() 242 | pass 243 | 244 | -------------------------------------------------------------------------------- /Editor/rshell.py.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1266884700a826b4d9dcbc4824f2fc57 3 | DefaultImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/rshell_install.bat: -------------------------------------------------------------------------------- 1 | pip install prompt_toolkit 2 | pip install pyfiglet -------------------------------------------------------------------------------- /Editor/rshell_install.bat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0e140111e0ab1904a9fa3e51f812b829 3 | DefaultImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/rshell_local.bat: -------------------------------------------------------------------------------- 1 | python %~dp0rshell.py -------------------------------------------------------------------------------- /Editor/rshell_local.bat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8b08c88e1176b0246b8fa70da097bf9f 3 | DefaultImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/rshell_remote.bat: -------------------------------------------------------------------------------- 1 | set /p address=please input remote address: 2 | python %~dp0rshell.py %address% 3 | pause -------------------------------------------------------------------------------- /Editor/rshell_remote.bat.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0a091056f1688784facfbfadd1134f3d 3 | DefaultImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RShell 2 | 在Unity游戏开发的过程中,我们常常会有这样的调试需求,能在运行时(包括真机)查看一些属性值,调用一些函数,RShell是解决这个问题的一个小工具。 3 | https://zhuanlan.zhihu.com/p/649662811 4 | 5 | ## 需要python3环境,使用pip安装依赖库 6 | ```Shell 7 | pip install prompt_toolkit 8 | pip install pyfiglet 9 | ``` 10 | 11 | ## Editor目录下commands可以自行预设一些指令输入提示 12 | ![image](https://github.com/user-attachments/assets/bd1efe9d-fef7-4ba3-bb52-deff9821667e) 13 | 14 | ## 操作演示 15 | ![Honeycam 2024-09-19 17-00-59](https://github.com/user-attachments/assets/23f4ddab-e3a6-4ab1-b96d-f0fd0b743cad) 16 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 364903b5af09c694eb13155b6cbc581f 3 | TextScriptImporter: 4 | externalObjects: {} 5 | supportLOD: 0 6 | lOD: 0 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cf4742b72f302764c9f6289b2740ca5f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | supportLOD: 0 7 | lOD: 0 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Runtime/CmdHelper.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d64a816958594a9d8173fbeaa02072d5 3 | timeCreated: 1689836332 -------------------------------------------------------------------------------- /Runtime/CmdHelper/Dumper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Reflection; 4 | using System.Text; 5 | using UnityEngine; 6 | 7 | namespace RShell 8 | { 9 | public static class Dumper 10 | { 11 | private static StringBuilder m_TextBuilder; 12 | 13 | private static StringBuilder TextBuilder 14 | { 15 | get 16 | { 17 | if (m_TextBuilder == null) 18 | { 19 | m_TextBuilder = new StringBuilder("", 1024); 20 | } 21 | 22 | return m_TextBuilder; 23 | } 24 | } 25 | 26 | public static string Do(object obj) 27 | { 28 | TextBuilder.Clear(); 29 | 30 | try 31 | { 32 | DoDump(obj); 33 | } 34 | catch (Exception e) 35 | { 36 | TextBuilder.Append($"Dump Failed: {e.Message}"); 37 | } 38 | 39 | return TextBuilder.ToString(); 40 | } 41 | 42 | private static void DoDump(object obj) 43 | { 44 | if (obj == null) 45 | { 46 | TextBuilder.Append("null"); 47 | return; 48 | } 49 | 50 | if (obj is Type) 51 | { 52 | TextBuilder.Append(((Type)obj).FullName); 53 | return; 54 | } 55 | 56 | Type t = obj.GetType(); 57 | 58 | // Repeat field 59 | if (obj is IList) 60 | { 61 | var list = obj as IList; 62 | TextBuilder.Append("["); 63 | foreach (object v in list) 64 | { 65 | DoDump(v); 66 | TextBuilder.Append(", "); 67 | } 68 | 69 | TextBuilder.Append("]"); 70 | } 71 | else if (t.IsValueType) 72 | { 73 | TextBuilder.Append(obj); 74 | } 75 | else if (obj is string) 76 | { 77 | TextBuilder.Append("\""); 78 | TextBuilder.Append(obj); 79 | TextBuilder.Append("\""); 80 | } 81 | else if (obj is IDictionary) 82 | { 83 | var dic = obj as IDictionary; 84 | TextBuilder.Append("{"); 85 | foreach (DictionaryEntry item in dic) 86 | { 87 | DoDump(item.Key); 88 | TextBuilder.Append(":"); 89 | DoDump(item.Value); 90 | TextBuilder.Append(", "); 91 | } 92 | 93 | TextBuilder.Append("}"); 94 | } 95 | else if (t.IsClass) 96 | { 97 | TextBuilder.Append(t.Name); 98 | TextBuilder.Append("{"); 99 | DumpClassObject(t, obj); 100 | TextBuilder.Append("}"); 101 | } 102 | else 103 | { 104 | Debug.LogWarning($"unsupported type: {t.FullName}"); 105 | TextBuilder.Append(obj); 106 | } 107 | } 108 | 109 | private static void DumpClassObject(Type objectType, object target) 110 | { 111 | PropertyInfo[] properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance); 112 | foreach (PropertyInfo property in properties) 113 | { 114 | try 115 | { 116 | object value = property.GetValue(target); 117 | TextBuilder.Append($"{property.Name}={value} "); 118 | } 119 | catch (Exception e) 120 | { 121 | TextBuilder.Append($"{property.Name}=? "); 122 | } 123 | } 124 | 125 | FieldInfo[] fieldInfos = objectType.GetFields(BindingFlags.Public | BindingFlags.Instance); 126 | foreach (FieldInfo fieldInfo in fieldInfos) 127 | { 128 | try 129 | { 130 | object value = fieldInfo.GetValue(target); 131 | TextBuilder.Append($"{fieldInfo.Name}={value} "); 132 | } 133 | catch (Exception e) 134 | { 135 | TextBuilder.Append($"{fieldInfo.Name}=? "); 136 | } 137 | } 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /Runtime/CmdHelper/Dumper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5130898481087704ea7ff2ef49031681 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/CmdHelper/Operator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace RShell 4 | { 5 | public static class Operator 6 | { 7 | public static double Add(double a, double b) 8 | { 9 | return a + b; 10 | } 11 | 12 | public static double Sub(double a, double b) 13 | { 14 | return a - b; 15 | } 16 | 17 | public static double Mul(double a, double b) 18 | { 19 | return a * b; 20 | } 21 | 22 | public static object Index(IList obj, int index) 23 | { 24 | return obj[index]; 25 | } 26 | 27 | public static object Index(IDictionary obj, object key) 28 | { 29 | return obj[key]; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Runtime/CmdHelper/Operator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5da73a109a724cf5840248f405ace4b7 3 | timeCreated: 1689765691 -------------------------------------------------------------------------------- /Runtime/CmdHelper/Scene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace RShell 9 | { 10 | public static class Scene 11 | { 12 | public static string List() 13 | { 14 | return List(""); 15 | } 16 | 17 | public static string List(string path) 18 | { 19 | List targetGameObjects; 20 | if (string.IsNullOrEmpty(path)) 21 | { 22 | targetGameObjects = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects().ToList(); 23 | } 24 | else 25 | { 26 | Transform target = Find(path).transform; 27 | targetGameObjects = new List(); 28 | for (int i = 0; i < target.childCount; i++) 29 | targetGameObjects.Add(target.transform.GetChild(i).gameObject); 30 | } 31 | 32 | return string.Join(",", targetGameObjects.Select(go => go.name)); 33 | } 34 | 35 | public static GameObject Find(string path) 36 | { 37 | GameObject[] rootGameObjects = 38 | UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects(); 39 | var parts = path.Split('/'); 40 | string rootGameObjectName = path.Split('/')[0]; 41 | string pathFromRoot = parts.Length > 1 ? path.Substring(rootGameObjectName.Length + 1) : ""; 42 | var rootGameObject = rootGameObjects.First(go => go.name == rootGameObjectName); 43 | if (rootGameObject == null) return null; 44 | 45 | Transform target = string.IsNullOrEmpty(pathFromRoot) 46 | ? rootGameObject.transform 47 | : rootGameObject.transform.Find(pathFromRoot); 48 | return target == null ? null : target.gameObject; 49 | } 50 | 51 | public static string Info(string path) 52 | { 53 | var go = Find(path); 54 | if (go == null) return null; 55 | 56 | var sb = new StringBuilder(); 57 | sb.AppendLine($"name:{go.name}"); 58 | sb.AppendLine($"childCount:{go.transform.childCount}"); 59 | sb.AppendLine($"activeSelf:{go.activeSelf}"); 60 | sb.AppendLine($"activeInHierarchy:{go.activeInHierarchy}"); 61 | 62 | Component[] components = go.GetComponents(); 63 | sb.AppendLine($"components:{components.Length}"); 64 | 65 | foreach (var component in components) 66 | { 67 | sb.AppendLine($"{component.name} {Dumper.Do(component)}"); 68 | } 69 | 70 | return sb.ToString(); 71 | } 72 | 73 | public static string FindPlayingEffect() 74 | { 75 | var sb = new StringBuilder(); 76 | ParticleSystem[] pslist = Object.FindObjectsOfType(); 77 | int counter = 0; 78 | for (int i = 0; i < pslist.Length; i++) 79 | { 80 | var ps = pslist[i]; 81 | if (ps.isPlaying) 82 | { 83 | counter++; 84 | sb.AppendLine($"{GetTransformPath(ps.transform)} cullingMode:{ps.main.cullingMode} loop:{ps.main.loop}"); 85 | } 86 | } 87 | 88 | sb.AppendLine($"counter: {counter} / {pslist.Length}"); 89 | return sb.ToString(); 90 | } 91 | 92 | public static void ChangeEffectCullingMode(ParticleSystemCullingMode mode) 93 | { 94 | ParticleSystem[] pslist = Object.FindObjectsOfType(); 95 | 96 | for (int i = 0; i < pslist.Length; i++) 97 | { 98 | var ps = pslist[i]; 99 | var main = ps.main; 100 | main.cullingMode = mode; 101 | } 102 | } 103 | 104 | public static string GetTransformPath(Transform transform) 105 | { 106 | StringBuilder pathBuilder = new StringBuilder(128); // 预分配容量 107 | while (transform != null) 108 | { 109 | pathBuilder.Insert(0, transform.name); // 每次插入到最前面 110 | transform = transform.parent; 111 | if (transform != null) pathBuilder.Insert(0, "/"); 112 | } 113 | return pathBuilder.ToString(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Runtime/CmdHelper/Scene.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f5594cbb167444a0b9f4195195e6de1c 3 | timeCreated: 1689520990 -------------------------------------------------------------------------------- /Runtime/CmdHelper/TestEvaluator.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_GM 2 | 3 | using System.Collections.Generic; 4 | 5 | namespace RShell 6 | { 7 | /// 8 | /// For FunctionEvaluator UnitTest 9 | /// 10 | public class TestEvaluator : TestEvaluatorSuper 11 | { 12 | public class InnerClass 13 | { 14 | public class InnerInnerClass 15 | { 16 | public static int Value = 999; 17 | } 18 | 19 | public static int Value = 999; 20 | } 21 | 22 | public enum TestEnum 23 | { 24 | A = 0, 25 | B = 1, 26 | C = 2 27 | } 28 | 29 | private static int StaticPrivateValue; 30 | public static int StaticPublicValue; 31 | 32 | public static int StaticGetSetValue 33 | { 34 | get => StaticPrivateValue; 35 | set => StaticPrivateValue = value; 36 | } 37 | 38 | 39 | private static TestEvaluator m_Evaluator; 40 | public static TestEvaluator GetInstance() 41 | { 42 | if (m_Evaluator == null) m_Evaluator = new TestEvaluator(); 43 | return m_Evaluator; 44 | } 45 | 46 | public static float StaticAdd(float a, float b) 47 | { 48 | return a + b; 49 | } 50 | 51 | public static int TestOverload(int i) 52 | { 53 | return i + 1; 54 | } 55 | 56 | public static int TestDefaultValue(int i, int j = 1, int k = 1) 57 | { 58 | return i + j + k; 59 | } 60 | 61 | public static string TestObj(TestEvaluator a, string str, TestEvaluator b, int i, int j) 62 | { 63 | return string.Format("{0} {1} {2}", str, i, j); 64 | } 65 | 66 | public static string TestOverload(string str) 67 | { 68 | return str; 69 | } 70 | 71 | public static TestEnum TestEnumFunc(TestEnum e) 72 | { 73 | return e; 74 | } 75 | 76 | public static List TestList() 77 | { 78 | return new List() {5, 6, 7}; 79 | } 80 | 81 | public static Dictionary TestDic() 82 | { 83 | return new Dictionary() {{"a", 1}, {"b", 2}, {"c", 3}}; 84 | } 85 | 86 | 87 | public int PublicValue; 88 | public float PublicValue2; 89 | private int m_PrivateValue; 90 | 91 | public int GetSetValue 92 | { 93 | get => m_PrivateValue; 94 | set => m_PrivateValue = value; 95 | } 96 | 97 | public float Add(float a, float b) 98 | { 99 | return a + b; 100 | } 101 | } 102 | 103 | public class TestEvaluatorSuper 104 | { 105 | public int SuperValue = 3; 106 | } 107 | 108 | } 109 | 110 | #endif -------------------------------------------------------------------------------- /Runtime/CmdHelper/TestEvaluator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b3c310aa0b50437e88391001e41161dd 3 | timeCreated: 1689496650 -------------------------------------------------------------------------------- /Runtime/FunctionEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace RShell 8 | { 9 | public class FunctionEvaluator 10 | { 11 | private enum SyntaxType 12 | { 13 | MethodCall, 14 | ValueSet, 15 | ValueGet 16 | } 17 | 18 | private Assembly[] m_Assemblies = null; 19 | private StringBuilder m_StringBuilder = null; 20 | private readonly Dictionary m_TypeCache = new Dictionary(); 21 | 22 | private readonly List m_GlobalEnvironmentNameSpace = new List() 23 | { 24 | "RShell", 25 | "UnityEngine" 26 | }; 27 | 28 | public void AddGlobalEnvironmentNameSpace(string nameSpace) 29 | { 30 | m_GlobalEnvironmentNameSpace.Add(nameSpace); 31 | } 32 | 33 | public bool Execute(string code, out object returnObj) 34 | { 35 | m_Assemblies = AppDomain.CurrentDomain.GetAssemblies(); 36 | if (m_StringBuilder == null) m_StringBuilder = new StringBuilder(); 37 | return ExecuteInnernal(code, out returnObj); 38 | } 39 | 40 | private bool ExecuteInnernal(string code, out object returnObj) 41 | { 42 | returnObj = null; 43 | try 44 | { 45 | var parts = ParseCodePart(code.Trim()); 46 | returnObj = FindRootType(ref parts); 47 | if (returnObj == null) 48 | throw new Exception("Root type not found"); 49 | 50 | while (parts.Count > 0) 51 | { 52 | string p = parts[0]; 53 | parts.RemoveAt(0); 54 | 55 | Type targetType; 56 | object targetInstance; 57 | var type = returnObj as Type; 58 | if (type != null) 59 | { 60 | targetType = type; 61 | targetInstance = null; 62 | } 63 | else 64 | { 65 | if (returnObj == null) 66 | throw new Exception($"Target instance is null when executing {p}"); 67 | 68 | targetType = returnObj.GetType(); 69 | targetInstance = returnObj; 70 | } 71 | 72 | switch (CheckSyntaxType(p)) 73 | { 74 | case SyntaxType.MethodCall: 75 | returnObj = ExecuteMethodCall(targetType, targetInstance, p); 76 | break; 77 | case SyntaxType.ValueGet: 78 | returnObj = ExecuteValueGet(targetType, targetInstance, p); 79 | break; 80 | case SyntaxType.ValueSet: 81 | returnObj = ExecuteValueSet(targetType, targetInstance, p); 82 | break; 83 | } 84 | } 85 | 86 | } 87 | catch (Exception ex) 88 | { 89 | string innterEx = ""; 90 | if(ex.InnerException != null) 91 | { 92 | innterEx = $"InnerException: {ex.InnerException.Message}\n{ex.InnerException.StackTrace}"; 93 | } 94 | 95 | returnObj = $"Error executing code: {code}\n{ex.Message}\n{ex.StackTrace}\n{innterEx}"; 96 | return false; 97 | } 98 | 99 | return true; 100 | } 101 | 102 | 103 | private List ParseCodePart(string code) 104 | { 105 | var parts = new List(SyntaxSplit('.', code)); 106 | return parts; 107 | } 108 | 109 | private List SyntaxSplit(char splitChar, string code) 110 | { 111 | var parts = new List(); 112 | int j = 0; 113 | int inBracket = 0; 114 | bool inString = false; 115 | bool afterAssign = false; 116 | for (int i = 0; i < code.Length; i++) 117 | { 118 | if (code[i] == '"') 119 | { 120 | inString = !inString; 121 | } 122 | 123 | if (!inString) 124 | { 125 | if (code[i] == ';') 126 | { 127 | parts.Add(code.Substring(j, i - j)); 128 | return parts; 129 | } 130 | else if (code[i] == '(') 131 | { 132 | inBracket ++; 133 | } 134 | else if (code[i] == ')') 135 | { 136 | inBracket --; 137 | } 138 | else if(code[i] == '=') 139 | { 140 | afterAssign = true; 141 | } 142 | } 143 | 144 | if (i == code.Length - 1) 145 | { 146 | parts.Add(code.Substring(j, code.Length - j)); 147 | } 148 | else if (code[i] == splitChar && inBracket == 0 && !afterAssign && !inString) 149 | { 150 | parts.Add(code.Substring(j, i - j)); 151 | j = i + 1; 152 | } 153 | } 154 | 155 | return parts; 156 | } 157 | 158 | private SyntaxType CheckSyntaxType(string code) 159 | { 160 | if (code.Contains("=")) 161 | return SyntaxType.ValueSet; 162 | 163 | if (code.Contains("(") && code.Contains(")")) 164 | return SyntaxType.MethodCall; 165 | 166 | return SyntaxType.ValueGet; 167 | } 168 | 169 | private Type FindRootType(ref List parts) 170 | { 171 | var root = _FindRootType(ref parts, false); 172 | if (root == null) 173 | { 174 | root = _FindRootType(ref parts, true); 175 | } 176 | 177 | return root; 178 | } 179 | 180 | private Type _FindRootType(ref List parts, bool useGlobalNamespace) 181 | { 182 | for (int i = 0; i < parts.Count; i++) 183 | { 184 | int testCount = parts.Count - i; 185 | if (useGlobalNamespace) 186 | { 187 | foreach (var globalNameSpace in m_GlobalEnvironmentNameSpace) 188 | { 189 | var root = TestType(parts, testCount, globalNameSpace); 190 | if (root != null) 191 | { 192 | parts.RemoveRange(0, testCount); 193 | return root; 194 | } 195 | } 196 | } 197 | else 198 | { 199 | var root = TestType(parts, testCount); 200 | if (root != null) 201 | { 202 | parts.RemoveRange(0, testCount); 203 | return root; 204 | } 205 | } 206 | } 207 | 208 | return null; 209 | } 210 | 211 | private Type TestType(List parts, int count, string globalNameSpace = null) 212 | { 213 | int maxInnerClassCount = count - 1; 214 | string originTypeName = null; 215 | for (int innerClassCount = 0; innerClassCount <= maxInnerClassCount; innerClassCount++) 216 | { 217 | var testTypeName = GetTypeName(parts, count, innerClassCount, globalNameSpace); 218 | if (originTypeName == null) 219 | { 220 | originTypeName = testTypeName; 221 | if (m_TypeCache.TryGetValue(originTypeName, out var value)) 222 | { 223 | if (value != null) 224 | return value; 225 | } 226 | } 227 | 228 | foreach (Assembly assembly in m_Assemblies) 229 | { 230 | var type = assembly.GetType(testTypeName); 231 | if (type != null) 232 | { 233 | m_TypeCache[originTypeName] = type; 234 | return type; 235 | } 236 | } 237 | } 238 | 239 | m_TypeCache[originTypeName] = null; 240 | return null; 241 | } 242 | 243 | private string GetTypeName(List parts, int count, int innerClassCount, string globalNameSpace) 244 | { 245 | m_StringBuilder.Clear(); 246 | 247 | if (!string.IsNullOrEmpty(globalNameSpace)) 248 | { 249 | m_StringBuilder.Append(globalNameSpace); 250 | m_StringBuilder.Append("."); 251 | } 252 | 253 | int innerClassIndex = count - 1 - innerClassCount; 254 | for (int i = 0; i < count; i++) 255 | { 256 | m_StringBuilder.Append(parts[i]); 257 | if (i != count - 1) 258 | { 259 | if (i >= innerClassIndex) 260 | m_StringBuilder.Append("+"); 261 | else 262 | m_StringBuilder.Append("."); 263 | } 264 | 265 | } 266 | return m_StringBuilder.ToString(); 267 | } 268 | 269 | private object ProcessParameter(string parameterStr) 270 | { 271 | if (parameterStr == "null") return null; 272 | 273 | if (parameterStr.Length > 1 && parameterStr.StartsWith("\"") && parameterStr.EndsWith("\"")) 274 | { 275 | var str = parameterStr.Substring(1, parameterStr.Length - 2); 276 | return str; 277 | } 278 | 279 | 280 | if (!double.TryParse(parameterStr, out _) && parameterStr.Contains(".")) 281 | { 282 | object returnObj; 283 | if (Execute(parameterStr, out returnObj)) 284 | return returnObj; 285 | throw new Exception($"ProcessParameter failed:\n{returnObj}"); 286 | } 287 | 288 | return parameterStr; 289 | } 290 | 291 | #region ValueGetSet 292 | 293 | private void ExtractSetParameter(string code, out string leftStr, out object rightObj) 294 | { 295 | int i = code.IndexOf('='); 296 | leftStr = code.Substring(0, i).Trim(); 297 | 298 | var rightStr = code.Substring(i + 1).Trim(); 299 | rightObj = ProcessParameter(rightStr); 300 | } 301 | 302 | private object ExecuteValueSet(Type targetType, object targetInstance, string code) 303 | { 304 | string leftStr; 305 | object rightObj; 306 | ExtractSetParameter(code, out leftStr, out rightObj); 307 | 308 | BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | 309 | BindingFlags.NonPublic | BindingFlags.SetField | BindingFlags.SetProperty; 310 | 311 | PropertyInfo propertyInfo = targetType.GetProperty(leftStr, flags); 312 | if (propertyInfo != null) 313 | { 314 | propertyInfo.SetValue(targetInstance, Convert.ChangeType(rightObj, propertyInfo.PropertyType)); 315 | return propertyInfo.GetValue(targetInstance); 316 | } 317 | 318 | FieldInfo fieldInfo = targetType.GetField(leftStr, flags); 319 | if (fieldInfo != null) 320 | { 321 | fieldInfo.SetValue(targetInstance, Convert.ChangeType(rightObj, fieldInfo.FieldType)); 322 | return fieldInfo.GetValue(targetInstance); 323 | } 324 | 325 | throw new Exception($"Can't find property or field {leftStr} in type {targetType}"); 326 | } 327 | 328 | private object ExecuteValueGet(Type targetType, object targetInstance, string code) 329 | { 330 | BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | 331 | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.FlattenHierarchy; 332 | PropertyInfo propertyInfo = targetType.GetProperty(code, flags); 333 | if (propertyInfo != null) 334 | { 335 | return propertyInfo.GetValue(targetInstance); 336 | } 337 | 338 | FieldInfo fieldInfo = targetType.GetField(code, flags); 339 | if (fieldInfo != null) 340 | { 341 | return fieldInfo.GetValue(targetInstance); 342 | } 343 | 344 | throw new Exception($"Can't find property or field {code} in type {targetType}"); 345 | } 346 | 347 | #endregion 348 | 349 | #region MethodCall 350 | 351 | private object ExecuteMethodCall(Type targetType, object targetInstance, string code) 352 | { 353 | string methodName; 354 | object[] inputObjs; 355 | ExtractFunctionCall(code, out methodName, out inputObjs); 356 | 357 | var methods = targetType.GetMethods(); 358 | foreach (var method in methods) 359 | { 360 | if (method.Name != methodName) 361 | continue; 362 | 363 | ParameterInfo[] methodParameterInfos = method.GetParameters(); 364 | if (methodParameterInfos.Length < inputObjs.Length) 365 | continue; 366 | 367 | object[] parameters = new object[methodParameterInfos.Length]; 368 | 369 | try 370 | { 371 | for (int i = 0; i < methodParameterInfos.Length; i++) 372 | { 373 | ParameterInfo expectedInfo = methodParameterInfos[i]; 374 | if(i >= inputObjs.Length) 375 | { 376 | parameters[i] = expectedInfo.HasDefaultValue ? expectedInfo.DefaultValue : null; 377 | continue; 378 | } 379 | 380 | object inputObj = inputObjs[i]; 381 | if (inputObjs[i] == null) 382 | { 383 | parameters[i] = null; 384 | continue; 385 | } 386 | 387 | if (expectedInfo.ParameterType.IsInstanceOfType(inputObj)) 388 | { 389 | parameters[i] = inputObj; 390 | } 391 | else 392 | { 393 | int enumValue; 394 | if(expectedInfo.ParameterType.IsEnum && int.TryParse(inputObj.ToString(), out enumValue)) 395 | { 396 | parameters[i] = Enum.ToObject(expectedInfo.ParameterType, enumValue); 397 | } 398 | else 399 | { 400 | parameters[i] = Convert.ChangeType(inputObj, expectedInfo.ParameterType); 401 | } 402 | } 403 | } 404 | } 405 | catch (Exception e) 406 | { 407 | continue; 408 | } 409 | 410 | return method.Invoke(targetInstance, parameters); 411 | } 412 | 413 | throw new Exception($"Can't find method {targetType} {methodName} {Dumper.Do(inputObjs)}"); 414 | } 415 | 416 | private void ExtractFunctionCall(string code, out string methodName, out object[] parameters) 417 | { 418 | var startIndex = code.IndexOf('('); 419 | var endIndex = code.LastIndexOf(')'); 420 | methodName = code.Substring(0, startIndex).Trim(); 421 | var parameterStr = code.Substring(startIndex + 1, endIndex - startIndex - 1).Trim(); 422 | if (string.IsNullOrEmpty(parameterStr)) 423 | { 424 | parameters = Array.Empty(); 425 | } 426 | else 427 | { 428 | var list = new List(); 429 | 430 | var parameterStrArr = SyntaxSplit(',', parameterStr).Select(x => x.Trim()); 431 | foreach (var str in parameterStrArr) 432 | { 433 | list.Add(ProcessParameter(str)); 434 | } 435 | 436 | parameters = list.ToArray(); 437 | } 438 | } 439 | 440 | #endregion 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /Runtime/FunctionEvaluator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: abfc4c2b9be346dcbc7d07247f14dd14 3 | timeCreated: 1689486124 -------------------------------------------------------------------------------- /Runtime/Shell.Test.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_GM 2 | 3 | using System; 4 | using System.Collections; 5 | using System.Text; 6 | using UnityEngine; 7 | using UnityEngine.Assertions; 8 | 9 | namespace RShell 10 | { 11 | public static partial class Shell 12 | { 13 | 14 | public static string TestSelf() 15 | { 16 | try 17 | { 18 | object result; 19 | float num; 20 | 21 | result = Execute("UnityEngine.Application.targetFrameRate;f"); 22 | Assert.IsTrue(float.TryParse(result.ToString(), out _)); 23 | 24 | result = Execute("Time.time;"); 25 | Assert.IsTrue(float.TryParse(result.ToString(), out _)); 26 | 27 | result = Execute("RShell.TestEvaluator.StaticAdd(1.0, 2.9)"); 28 | num = float.Parse(result.ToString()); 29 | Assert.IsTrue(num == 3.9f); 30 | 31 | result = Execute("TestEvaluator.GetInstance().Add(1.0, 2.9);"); 32 | num = float.Parse(result.ToString()); 33 | Assert.IsTrue(num == 3.9f); 34 | 35 | result = Execute("TestEvaluator.StaticPrivateValue = 1"); 36 | Assert.IsTrue(result.ToString() == "1"); 37 | 38 | result = Execute("TestEvaluator.StaticPublicValue = 1"); 39 | Assert.IsTrue(result.ToString() == "1"); 40 | 41 | result = Execute("TestEvaluator.StaticGetSetValue = 1; "); 42 | Assert.IsTrue(result.ToString() == "1"); 43 | 44 | result = Execute("TestEvaluator.GetInstance().PublicValue = 1"); 45 | Assert.IsTrue(result.ToString() == "1"); 46 | 47 | result = Execute("TestEvaluator.GetInstance().m_PrivateValue = 1"); 48 | Assert.IsTrue(result.ToString() == "1"); 49 | 50 | result = Execute("TestEvaluator.GetInstance().GetSetValue = 1"); 51 | Assert.IsTrue(result.ToString() == "1"); 52 | 53 | result = Execute("TestEvaluator.StaticPrivateValue;"); 54 | Assert.IsTrue(result.ToString() == "1"); 55 | 56 | result = Execute("TestEvaluator.StaticPublicValue "); 57 | Assert.IsTrue(result.ToString() == "1"); 58 | 59 | result = Execute("TestEvaluator.StaticGetSetValue"); 60 | Assert.IsTrue(result.ToString() == "1"); 61 | 62 | result = Execute("TestEvaluator.GetInstance().PublicValue "); 63 | Assert.IsTrue(result.ToString() == "1"); 64 | 65 | result = Execute("TestEvaluator.GetInstance().m_PrivateValue"); 66 | Assert.IsTrue(result.ToString() == "1"); 67 | 68 | result = Execute("TestEvaluator.GetInstance().GetSetValue"); 69 | Assert.IsTrue(result.ToString() == "1"); 70 | 71 | result = Execute("TestEvaluator.GetInstance().SuperValue"); 72 | Assert.IsTrue(result.ToString() == "3"); 73 | 74 | result = Execute("TestEvaluator.StaticAdd(RShell.TestEvaluator.StaticPublicValue, TestEvaluator.StaticAdd(1, 1))"); 75 | Assert.IsTrue(result.ToString() == "3"); 76 | 77 | Execute("TestEvaluator.StaticPublicValue = TestEvaluator.StaticAdd(1,1)"); 78 | result = Execute("TestEvaluator.StaticPublicValue"); 79 | Assert.IsTrue(result.ToString() == "2"); 80 | 81 | Execute(" TestEvaluator.StaticPublicValue = TestEvaluator.TestOverload(TestEvaluator.StaticAdd(10,10))"); 82 | result = Execute("TestEvaluator.StaticPublicValue"); 83 | Assert.IsTrue(result.ToString() == "21"); 84 | 85 | result = Execute("TestEvaluator.TestOverload(\"TestEvaluator.StaticAdd(10,10)\")"); 86 | Assert.IsTrue(result.ToString() == "TestEvaluator.StaticAdd(10,10)"); 87 | 88 | result = Execute("TestEvaluator.TestObj(TestEvaluator.GetInstance(), \"hello world\", TestEvaluator.GetInstance(), 10, 10);"); 89 | Assert.IsTrue(result.ToString() == "hello world 10 10"); 90 | 91 | result = Execute("TestEvaluator.TestDefaultValue(1)"); 92 | Assert.IsTrue(result.ToString() == "3"); 93 | 94 | result = Execute("TestEvaluator.GetInstance()"); 95 | Assert.IsNotNull(result); 96 | 97 | Execute("TestEvaluator.m_Evaluator = null"); 98 | result = Execute("TestEvaluator.m_Evaluator"); 99 | Assert.IsNull(result); 100 | 101 | result = Execute("TestEvaluator.TestEnumFunc(2)"); 102 | Assert.IsTrue(result.ToString() == "C"); 103 | 104 | result = Execute("TestEvaluator.TestEnumFunc(TestEvaluator.TestEnum.A)"); 105 | Assert.IsTrue(result.ToString() == "A"); 106 | 107 | result = Execute("TestEvaluator.InnerClass.Value"); 108 | Assert.IsTrue(result.ToString() == "999"); 109 | 110 | result = Execute("TestEvaluator.InnerClass.InnerInnerClass.Value"); 111 | Assert.IsTrue(result.ToString() == "999"); 112 | 113 | result = Execute("Operator.Index(TestEvaluator.TestList(), 1)"); 114 | Assert.IsTrue(result.ToString() == "6"); 115 | 116 | result = Execute("Operator.Index(TestEvaluator.TestDic(), \"b\")"); 117 | Assert.IsTrue(result.ToString() == "2"); 118 | 119 | return "Test Complete!"; 120 | } 121 | catch (Exception e) 122 | { 123 | return $"{e.Message}\n{e.StackTrace}"; 124 | } 125 | } 126 | 127 | 128 | public static string TestLargeStr(int count) 129 | { 130 | var sb = new StringBuilder(); 131 | for (int i = 0; i < count; i++) 132 | { 133 | sb.AppendLine($"TestLargeStr: {i}"); 134 | } 135 | 136 | return sb.ToString(); 137 | } 138 | 139 | 140 | } 141 | } 142 | 143 | #endif 144 | -------------------------------------------------------------------------------- /Runtime/Shell.Test.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 875a53b6860e4ed0bd52d796cbb20ff0 3 | timeCreated: 1689766555 -------------------------------------------------------------------------------- /Runtime/Shell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEngine; 4 | 5 | namespace RShell 6 | { 7 | public static partial class Shell 8 | { 9 | public static bool EnableExecLog = false; 10 | private static FunctionEvaluator m_FunctionEvaluator; 11 | private static UdpHost m_UdpHost; 12 | 13 | private static FunctionEvaluator FunctionEvaluator 14 | { 15 | get 16 | { 17 | if (m_FunctionEvaluator == null) 18 | m_FunctionEvaluator = new FunctionEvaluator(); 19 | return m_FunctionEvaluator; 20 | } 21 | } 22 | 23 | public static int SendMTU 24 | { 25 | get 26 | { 27 | return m_UdpHost.SendMTU; 28 | } 29 | set 30 | { 31 | m_UdpHost.SendMTU = value; 32 | } 33 | } 34 | 35 | public static int SendInterval 36 | { 37 | get 38 | { 39 | return m_UdpHost.SendInterval; 40 | } 41 | set 42 | { 43 | m_UdpHost.SendInterval = value; 44 | } 45 | } 46 | 47 | public static void Listen(int port = 9999) 48 | { 49 | if (m_UdpHost == null) 50 | { 51 | try 52 | { 53 | m_UdpHost = new UdpHost(port); 54 | } 55 | catch (Exception e) 56 | { 57 | Debug.LogWarning("RShell Listen failed: " + e.Message); 58 | return; 59 | } 60 | 61 | m_UdpHost.MessageReceived += OnMessageReceived; 62 | m_UdpHost.Start(); 63 | Application.quitting += OnApplicationQuit; 64 | Debug.Log($"RShell Listening... {port}"); 65 | } 66 | else 67 | { 68 | Debug.Log("RShell is already listening.. "); 69 | } 70 | } 71 | 72 | public static void StopListen() 73 | { 74 | if (m_UdpHost != null) 75 | { 76 | m_UdpHost.Stop(); 77 | m_UdpHost = null; 78 | Application.quitting -= OnApplicationQuit; 79 | } 80 | } 81 | 82 | private static void OnApplicationQuit() 83 | { 84 | StopListen(); 85 | } 86 | 87 | private static void OnMessageReceived(string code) 88 | { 89 | if (string.IsNullOrEmpty(code)) return; 90 | object returnObj; 91 | FunctionEvaluator.Execute(code, out returnObj); 92 | var msg = returnObj == null ? "ok" : returnObj.ToString(); 93 | 94 | if(EnableExecLog) 95 | Debug.Log($"RShell Execute: {code}\n{returnObj}"); 96 | 97 | m_UdpHost.Send(msg, code); 98 | } 99 | 100 | public static object Execute(string code) 101 | { 102 | object returnObj; 103 | if (FunctionEvaluator.Execute(code, out returnObj)) 104 | return returnObj; 105 | throw new Exception($"RShell execute failed \n{returnObj}"); 106 | } 107 | 108 | public static void AddGlobalEnvironmentNameSpace(string nameSpace) 109 | { 110 | FunctionEvaluator.AddGlobalEnvironmentNameSpace(nameSpace); 111 | } 112 | 113 | public static int LoadLibrary(string libname) 114 | { 115 | string exPath = $"sdcard/Android/data/{Application.identifier}/files/{libname}"; 116 | string appPath = $"/data/data/{Application.identifier}/{libname}"; 117 | bool exPathExists = File.Exists(exPath); 118 | bool appPathExists = File.Exists(appPath); 119 | if(!exPathExists && !appPathExists) 120 | { 121 | return 1; 122 | } 123 | 124 | if(exPathExists) 125 | { 126 | File.Copy(exPath, appPath, true); 127 | } 128 | 129 | var systemClass = new AndroidJavaClass("java.lang.System"); 130 | try 131 | { 132 | systemClass.CallStatic("load", appPath); 133 | } 134 | catch(Exception e) 135 | { 136 | Debug.LogException(e); 137 | return 2; 138 | } 139 | 140 | return 0; 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Runtime/Shell.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 905dbb57e72e4eada1d5b8f1d5c68303 3 | timeCreated: 1689485814 -------------------------------------------------------------------------------- /Runtime/UdpHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading; 7 | using UnityEngine; 8 | 9 | namespace RShell 10 | { 11 | public class UdpHost 12 | { 13 | [Serializable] 14 | private class Message 15 | { 16 | public int MsgId; 17 | public int FragIndex; 18 | public int FragCount; 19 | public string Content; 20 | public string CheckMsg; 21 | } 22 | 23 | 24 | public int SendMTU = 1000; // bytes 25 | public int SendInterval = 100; // ms 26 | 27 | public event Action MessageReceived; 28 | public event Action OnThreadStart; 29 | 30 | private readonly UdpClient m_UdpClient; 31 | private readonly ConcurrentQueue m_MessagesToSend = new ConcurrentQueue(); 32 | private bool m_IsSending; 33 | 34 | private IPEndPoint m_ReceiveRemoteEndPoint; 35 | private IPEndPoint m_SendRemoteEndPoint; 36 | 37 | private SynchronizationContext m_MainContext; 38 | private Thread m_Thread; 39 | private int m_MsgId; 40 | 41 | public UdpHost(int localPort) 42 | { 43 | m_UdpClient = new UdpClient(localPort); 44 | } 45 | 46 | public void Send(string content, string checkMsg) 47 | { 48 | m_MsgId++; 49 | 50 | if (content.Length < SendMTU) 51 | { 52 | AddToSendQueue(content, m_MsgId, checkMsg); 53 | } 54 | else 55 | { 56 | var total = Mathf.CeilToInt(content.Length / (float) SendMTU); 57 | for (int i = 0; i < total; i++) 58 | { 59 | var index = i * SendMTU; 60 | var len = Mathf.Min(SendMTU, content.Length - index); 61 | AddToSendQueue(content.Substring(index, len), m_MsgId, checkMsg, i, total); 62 | } 63 | } 64 | } 65 | 66 | private void AddToSendQueue(string content, int msgId, string checkMsg, int fragIndex = 0, int fragCount = 1) 67 | { 68 | var message = new Message 69 | { 70 | Content = content, 71 | MsgId = msgId, 72 | FragIndex = fragIndex, 73 | FragCount = fragCount, 74 | CheckMsg = checkMsg 75 | }; 76 | 77 | m_MessagesToSend.Enqueue(message); 78 | 79 | if(!m_IsSending) 80 | SendAsync(); 81 | } 82 | 83 | async void SendAsync() 84 | { 85 | m_IsSending = true; 86 | 87 | try 88 | { 89 | Message message; 90 | while (m_MessagesToSend.TryDequeue(out message)) 91 | { 92 | var sendBytes = Encoding.UTF8.GetBytes(JsonUtility.ToJson(message)); 93 | lock (this) 94 | { 95 | m_UdpClient.Send(sendBytes, sendBytes.Length, m_SendRemoteEndPoint); 96 | } 97 | 98 | await System.Threading.Tasks.Task.Delay(SendInterval); 99 | } 100 | } 101 | catch (Exception e) 102 | { 103 | Debug.LogError(e.Message); 104 | } 105 | finally 106 | { 107 | m_IsSending = false; 108 | } 109 | } 110 | 111 | public void Start() 112 | { 113 | m_MainContext = SynchronizationContext.Current; 114 | m_Thread = new Thread(Run) { Name = "RShell", Priority = System.Threading.ThreadPriority.Highest}; 115 | m_Thread.Start(); 116 | } 117 | 118 | public void Stop() 119 | { 120 | m_UdpClient?.Close(); 121 | m_Thread?.Abort(); 122 | } 123 | 124 | private void Run() 125 | { 126 | m_ReceiveRemoteEndPoint = new IPEndPoint(IPAddress.Any, 0); 127 | if(OnThreadStart != null) OnThreadStart.Invoke(); 128 | 129 | while (true) 130 | { 131 | try 132 | { 133 | var receiveBytes = m_UdpClient.Receive(ref m_ReceiveRemoteEndPoint); 134 | lock (this) 135 | { 136 | m_SendRemoteEndPoint = m_ReceiveRemoteEndPoint; 137 | } 138 | 139 | OnReceiveMessage(receiveBytes); 140 | } 141 | catch (Exception e) 142 | { 143 | if(e is ThreadAbortException) 144 | return; 145 | Debug.LogError(e.Message); 146 | } 147 | 148 | Thread.Sleep(200); 149 | } 150 | } 151 | 152 | private void OnReceiveMessage(byte[] data) 153 | { 154 | if (data == null) 155 | { 156 | return; 157 | } 158 | 159 | var text = Encoding.UTF8.GetString(data, 0, data.Length); 160 | if (text == "hi") 161 | { 162 | Send("welcome", text); 163 | } 164 | else 165 | { 166 | m_MainContext.Post((_) => 167 | { 168 | MessageReceived?.Invoke(text); 169 | }, null); 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Runtime/UdpHost.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0399eeeeaf9844bf91709a8904abb9d2 3 | timeCreated: 1689504619 --------------------------------------------------------------------------------