【Unity】通用GM QA工具 运行时数值修改 命令行 测试工具

news2024/11/24 18:51:23

GM工具使用:

 

GM工具通常用于游戏运行时修改数值(加钱/血量)、解锁关卡等,用于快速无死角测试游戏。一个通用型GM工具对于游戏项目是非常实用且必要的,但通用不能向易用妥协,纯命令行GM门槛太高,对QA不友好。

这类运行时命令行工具实现原理很简单,主要是通过给变量或方法添加Attribute标识,然后通过反射获取被标记的变量或方法,命令触发通过反射为变量赋值或Invoke方法。

此类工具免费或付费的已经泛滥了,不推荐浪费时间重复造轮子。

1. 免费开源的Log显示工具,也嵌入了命令行功能。由于GF有更好用的Debuger窗口了,所以没选择它:https://github.com/yasirkula/UnityIngameDebugConsole

2.  Quantum Console, 收费,AssetStore上好评最多,但强行绑定了一个UGUI界面,无解耦。这里我是想直接扩展进GF Debuger窗口,方便使用,因此需要修改插件源码:Quantum Console | Utilities Tools | Unity Asset Store

 感兴趣的话直接AssetStore搜“command console”,免费的也有很多。

我就不浪费时间筛选,直接选择购买好评较多的Quantum Console进行整改。

Quantum Console用法:

Quantum C默认只对继承MonoBehavior的脚本有效,应该是因为需要反射获取所有类型速度太慢,初始化时会卡顿。

对于继承自MonoBehavior的脚本直接通过以下Attribute标记命令即可:

1. 命令行前缀,CommandPrefix("GM."):

相当于给命令行分组,比如把所有命令行标记个前缀叫“GM.”, 那么输入"GM"时所有GM开头的命令都会在列表中显示出来。

[CommandPrefix("GM.玩家.")]
public class PlayerEntity
{
}

2. 把变量或方法作为命令行,Command("命令名字", "命令用法说明"):

[Command("移动速度", "float类型,默认值10")]
private float moveSpeed = 10f;
[Command("添加敌人", "参数int,创建敌人个数")]
internal void AddEnemies(int v)

 对于非MonoBehavior脚本需要手动调用注册命令接口,将该类型添加到需要反射扫描的名单里:

1. QuantumRegistry.RegisterObject()和QuantumRegistry.DeregisterObject()注册或取消注册,然后通过Command("命令名字", "命令描述", MonoTargetType.Registry)添加命令:

public class PlayerDataModel : DataModelBase
{
    protected override void OnCreate(RefParams userdata)
    {
        QuantumRegistry.RegisterObject(this);
    }
    protected override void OnRelease()
    {
        QuantumRegistry.DeregisterObject(this);
    }
    [Command("金币", "玩家金币数量", MonoTargetType.Registry)]
    public int Coins;
}

将Quantum C扩展进GF:

由于GF解耦做得非常好了,我们只需要自定义类实现GameFramework.Debugger.IDebuggerWindow接口就可以写自己的GUI界面和功能了。

1. 扩展Debuger菜单栏,编写GM工具交互界面:

using System.Collections.Generic;
using UnityEngine;
using GameFramework;
using GameFramework.Debugger;
using System;
using Cysharp.Threading.Tasks;
using GM.Utilities;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
namespace GM
{
    public class GMConsoleWindow : IDebuggerWindow
    {
        const string LogCommand = "{0}";
        const string LogSuccess = "<color=#2BD988>{0}</color>";
        const string LogFailed = "<color=#F22E2E>{0}</color>";
        const string InputFieldCtrlID = "Input";
        private int m_MaxLine = 100;
        private int m_MaxRecordInputHistory = 30;
        private Queue<GMLogNode> m_LogNodes;
        private LinkedList<string> m_InputHistoryList;
        private LinkedListNode<string> m_CurrentHistory = null;
        string m_InputText;
        string m_PreInputText;
        bool m_InputFocused;
        bool m_InputChanged;
        Vector2 m_ScrollPosition = Vector2.zero;
        Vector2 m_FilterScrollPosition = Vector2.zero;
        SuggestionStack m_CommandsFilter;
        SuggestorOptions m_FilterOptions;
        Rect inputRect = default;
        bool m_LogAppend;
        bool m_MoveCursorToEnd;
        GUIStyle m_CommandsFilterBtStyle;
        private readonly Type m_VoidTaskType = typeof(Task<>).MakeGenericType(Type.GetType("System.Threading.Tasks.VoidTaskResult"));
        private List<System.Threading.Tasks.Task> m_CurrentTasks;
        private List<IEnumerator<ICommandAction>> m_CurrentActions;

        public void Initialize(params object[] args)
        {
            if (!QuantumConsoleProcessor.TableGenerated)
            {
                QuantumConsoleProcessor.GenerateCommandTable(true);
            }
            m_InputHistoryList = new LinkedList<string>();
            m_LogNodes = new Queue<GMLogNode>();
            m_CurrentTasks = new List<System.Threading.Tasks.Task>();
            m_CurrentActions = new List<IEnumerator<ICommandAction>>();
            m_CommandsFilter = new SuggestionStack();
            m_FilterOptions = new SuggestorOptions()
            {
                CaseSensitive = false,
                CollapseOverloads = true,
                Fuzzy = true,
            };
        }

        public void OnDraw()
        {
            if (m_CommandsFilterBtStyle == null)
            {
                m_CommandsFilterBtStyle = new GUIStyle(GUI.skin.button)
                {
                    alignment = TextAnchor.MiddleLeft
                };
            }
            GUILayout.BeginVertical();
            {
                m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, "box");
                {
                    foreach (var logNode in m_LogNodes)
                    {
                        GUILayout.Label(logNode.LogMessage);
                    }
                    GUILayout.EndScrollView();
                }
                if (m_LogAppend)
                {
                    m_LogAppend = false;
                    m_ScrollPosition = new Vector2(0, float.MaxValue);
                }

                GUILayout.BeginHorizontal();
                {
                    GUI.enabled = QuantumConsoleProcessor.TableGenerated;
                    GUI.SetNextControlName(InputFieldCtrlID);
                    m_InputText = GUILayout.TextField(m_InputText);
                    if (Event.current.type == EventType.Repaint)
                    {
                        inputRect = GUILayoutUtility.GetLastRect();
                        if (m_MoveCursorToEnd)
                        {
                            m_MoveCursorToEnd = false;
                            MoveInputCursorToEnd();
                        }
                    }

                    m_InputFocused = (GUI.GetNameOfFocusedControl() == InputFieldCtrlID);
                    m_InputChanged = m_InputText != m_PreInputText;
                    if (m_InputChanged)
                    {
                        m_PreInputText = m_InputText;
                        m_CommandsFilter.UpdateStack(m_InputText, m_FilterOptions);
                    }
                    if (GUILayout.Button("Execute", GUILayout.Width(60)))
                    {
                        ExecuteCommand(m_InputText);
                    }
                    if (GUILayout.Button("Clear", GUILayout.Width(60)))
                    {
                        ClearLogs();
                    }
                    GUILayout.EndHorizontal();
                }
                GUILayout.EndVertical();
                if (m_InputFocused && m_CommandsFilter.TopmostSuggestionSet != null)
                {
                    if (Event.current.type == EventType.Repaint)
                    {
                        float maxHeight = GUILayoutUtility.GetLastRect().height - inputRect.height - 5f;
                        inputRect.height = Mathf.Clamp(m_CommandsFilter.TopmostSuggestionSet.Suggestions.Count * 30, maxHeight * 0.5f, maxHeight);
                        inputRect.position -= Vector2.up * (inputRect.height + 5f);
                    }
                    if (m_InputChanged)
                    {
                        m_FilterScrollPosition = Vector2.zero;
                    }
                    GUILayout.BeginArea(inputRect);
                    m_FilterScrollPosition = GUILayout.BeginScrollView(m_FilterScrollPosition, "box", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
                    {
                        GUILayout.BeginVertical(GUILayout.ExpandHeight(true));
                        {
                            foreach (var item in m_CommandsFilter.TopmostSuggestionSet.Suggestions)
                            {
                                if (GUILayout.Button(item.FullSignature, m_CommandsFilterBtStyle))
                                {
                                    m_MoveCursorToEnd = true;
                                    var fragments = m_InputText.Split(' ');
                                    if (fragments.Length >= 2)
                                    {
                                        m_InputText = string.Empty;
                                        for (int i = 0; i < fragments.Length - 1; i++)
                                        {
                                            m_InputText = Utility.Text.Format("{0}{1}{2}", m_InputText, i == 0 ? string.Empty : " ", fragments[i]);
                                        }
                                        m_InputText = Utility.Text.Format("{0} {1}", m_InputText, item.PrimarySignature);
                                    }
                                    else
                                    {
                                        m_InputText = item.PrimarySignature;
                                    }
                                }
                            }
                            GUILayout.EndVertical();
                        }
                        GUILayout.EndScrollView();
                    }
                    GUILayout.EndArea();
                }
            }
        }
        /// <summary>
        /// 输入框游标移动到尾部
        /// </summary>
        private void MoveInputCursorToEnd()
        {
            GUI.FocusControl(InputFieldCtrlID);

            // 获取当前TextEditor
            TextEditor editor = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl);
            if (editor != null)
            {
                editor.cursorIndex = m_InputText.Length;
                editor.selectIndex = m_InputText.Length;
            }
        }

        public void OnEnter()
        {
            QuantumRegistry.RegisterObject<GMConsoleWindow>(this);
        }

        public void OnLeave()
        {
            QuantumRegistry.DeregisterObject<GMConsoleWindow>(this);
        }

        public void OnUpdate(float elapseSeconds, float realElapseSeconds)
        {
            if (m_InputFocused)
            {
                if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
                    ExecuteCommand(m_InputText);

                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SelectInputHistory(false);
                }
                else if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SelectInputHistory(true);
                }
            }

            TasksUpdate();
            ActionsUpdate();
        }

        public void Shutdown()
        {

        }
        private void SelectInputHistory(bool upOrdown)
        {
            if (m_InputHistoryList.Count == 0) return;
            m_MoveCursorToEnd = true;
            if (upOrdown)
            {
                if (m_CurrentHistory == null || m_CurrentHistory.Previous == null)
                {
                    m_InputText = m_InputHistoryList.Last.Value;
                    m_CurrentHistory = m_InputHistoryList.Last;
                    return;
                }
                m_InputText = m_CurrentHistory.Previous.Value;
                m_CurrentHistory = m_CurrentHistory.Previous;
            }
            else
            {
                if (m_CurrentHistory == null || m_CurrentHistory.Next == null)
                {
                    m_InputText = m_InputHistoryList.First.Value;
                    m_CurrentHistory = m_InputHistoryList.First;
                    return;
                }
                m_InputText = m_CurrentHistory.Next.Value;
                m_CurrentHistory = m_CurrentHistory.Next;
            }
        }
        private void AppendLog(GMLogType logType, string logMessage)
        {
            m_LogNodes.Enqueue(GMLogNode.Create(logType, logMessage));
            while (m_LogNodes.Count > m_MaxLine)
            {
                ReferencePool.Release(m_LogNodes.Dequeue());
            }
            m_LogAppend = true;
        }
        [Command("clear", "清空GM日志", MonoTargetType.Registry)]
        private void ClearLogs()
        {
            m_LogNodes.Clear();
            m_ScrollPosition = Vector2.zero;
        }

        private void ExecuteCommand(string cmd, bool recordHistory = true)
        {
            if (string.IsNullOrWhiteSpace(cmd)) return;
            if (recordHistory) RecordInputHistory(cmd);
            AppendLog(GMLogType.Command, cmd);
            m_InputText = string.Empty;

            try
            {
                var commandResult = QuantumConsoleProcessor.InvokeCommand(cmd);
                if (commandResult != null)
                {
                    if (commandResult is IEnumerator<ICommandAction> enumeratorTp)
                    {
                        m_CurrentActions.Add(enumeratorTp);
                        ActionsUpdate();
                    }
                    else if (commandResult is IEnumerable<ICommandAction> enumerableTp)
                    {
                        m_CurrentActions.Add(enumerableTp.GetEnumerator());
                        ActionsUpdate();
                    }
                    else if (commandResult is UniTask task)
                    {
                        m_CurrentTasks.Add(task.AsTask());
                    }
                    else if (commandResult.GetType().Name == "UniTask`1")
                    {
                        var asTaskGenericMethod = typeof(UniTaskExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(item => item.Name == "AsTask" && item.IsGenericMethod);
                        Type uniTaskType = commandResult.GetType();
                        Type genericArgument = uniTaskType.GetGenericArguments()[0];
                        MethodInfo genericMethod = asTaskGenericMethod.MakeGenericMethod(genericArgument);
                        Task taskT = (Task)genericMethod.Invoke(null, new object[] { commandResult });
                        m_CurrentTasks.Add(taskT);
                    }
                    else
                    {
                        var resultType = commandResult.GetType();
                        if (resultType == typeof(string) || resultType.IsPrimitive)
                        {
                            AppendLog(GMLogType.Success, commandResult.ToString());
                        }
                        else
                        {
                            AppendLog(GMLogType.Success, Utility.Json.ToJson(commandResult));
                        }
                    }
                }
            }
            catch (System.Reflection.TargetInvocationException e)
            {
                AppendLog(GMLogType.Failed, e.Message);
            }
            catch (Exception e)
            {
                AppendLog(GMLogType.Failed, e.Message);
            }

        }
        private void RecordInputHistory(string cmd)
        {
            if (m_InputHistoryList.Count > 0 && m_InputHistoryList.First.Value == cmd) return;
            m_InputHistoryList.AddFirst(cmd);
            m_CurrentHistory = m_InputHistoryList.Last;
            while (m_InputHistoryList.Count > m_MaxRecordInputHistory)
            {
                m_InputHistoryList.RemoveLast();
            }
        }
        private void TasksUpdate()
        {
            for (int i = m_CurrentTasks.Count - 1; i >= 0; i--)
            {
                if (m_CurrentTasks[i].IsCompleted)
                {
                    if (m_CurrentTasks[i].IsFaulted)
                    {
                        foreach (Exception e in m_CurrentTasks[i].Exception.InnerExceptions)
                        {
                            AppendLog(GMLogType.Failed, e.Message);
                        }
                    }
                    else
                    {
                        Type taskType = m_CurrentTasks[i].GetType();
                        if (taskType.IsGenericTypeOf(typeof(Task<>)) && !m_VoidTaskType.IsAssignableFrom(taskType))
                        {
                            System.Reflection.PropertyInfo resultProperty = m_CurrentTasks[i].GetType().GetProperty("Result");
                            object result = resultProperty.GetValue(m_CurrentTasks[i]);
                            string log = Utility.Json.ToJson(result);
                            AppendLog(GMLogType.Success, log);
                        }
                    }

                    m_CurrentTasks.RemoveAt(i);
                }
            }
        }
        private void ActionsUpdate()
        {
            for (int i = m_CurrentActions.Count - 1; i >= 0; i--)
            {
                IEnumerator<ICommandAction> action = m_CurrentActions[i];

                try
                {
                    if (action.Execute() != ActionState.Running)
                    {
                        m_CurrentActions.RemoveAt(i);
                    }
                }
                catch (Exception e)
                {
                    m_CurrentActions.RemoveAt(i);
                    AppendLog(GMLogType.Failed, e.Message);
                    break;
                }
            }
        }
        private enum GMLogType
        {
            Command,
            Success,
            Failed
        }
        /// <summary>
        /// 日志记录结点。
        /// </summary>
        private sealed class GMLogNode : IReference
        {
            private GMLogType m_LogType;
            private string m_LogMessage;

            /// <summary>
            /// 初始化日志记录结点的新实例。
            /// </summary>
            public GMLogNode()
            {
                m_LogType = GMLogType.Failed;
                m_LogMessage = null;
            }

            /// <summary>
            /// 获取日志类型。
            /// </summary>
            public GMLogType LogType
            {
                get
                {
                    return m_LogType;
                }
            }

            /// <summary>
            /// 获取日志内容。
            /// </summary>
            public string LogMessage
            {
                get
                {
                    return m_LogMessage;
                }
            }

            /// <summary>
            /// 创建日志记录结点。
            /// </summary>
            /// <param name="logType">日志类型。</param>
            /// <param name="logMessage">日志内容。</param>
            /// <returns>创建的日志记录结点。</returns>
            public static GMLogNode Create(GMLogType logType, string logMessage)
            {
                GMLogNode logNode = ReferencePool.Acquire<GMLogNode>();
                logNode.m_LogType = logType;
                switch (logType)
                {
                    case GMLogType.Success:
                        logNode.m_LogMessage = Utility.Text.Format(LogSuccess, logMessage);
                        break;
                    case GMLogType.Failed:
                        logNode.m_LogMessage = Utility.Text.Format(LogFailed, logMessage);
                        break;
                    default:
                        logNode.m_LogMessage = Utility.Text.Format(LogCommand, logMessage);
                        break;
                }
                return logNode;
            }

            /// <summary>
            /// 清理日志记录结点。
            /// </summary>
            public void Clear()
            {
                m_LogType = GMLogType.Failed;
                m_LogMessage = null;
            }
        }
    }
}

2. 将自定义的GM工具界面注册进GF Debuger窗口:

GF.Debugger.RegisterDebuggerWindow("GM", new GM.GMConsoleWindow());

效果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2063825.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手动下载Sentinel-1卫星精密轨道数据

轨道信息对于InSAR&#xff08;干涉合成孔径雷达&#xff09;数据处理至关重要&#xff0c;因为它影响从初始图像配准到最终形变图像生成的整个过程。不准确的轨道信息会导致基线误差&#xff0c;这些误差会以残差条纹的形式出现在干涉图中。为了消除由轨道误差引起的系统性误差…

学习ComfyUI的一个网站:www.comflowy.com

学习ComfyUI&#xff0c;不仅仅是照搬别人的工作流来使用&#xff0c;重要的是能自己搭建工作流&#xff0c;而要能够熟练搭建&#xff0c;离不开对工作流中节点&#xff08;特别是重要节点&#xff09;的透彻理解。比如我自己&#xff0c;原来对 Lora 就十分陌生&#xff0c;不…

echart改变legend样式及分页

legend: {type: "scroll",orient: horizontal, // 纵向&#xff0c;默认横向不用写pageIconColor: #1b9aee, //翻页下一页的三角按钮颜色pageIconInactiveColor: #7f7f7f, //翻页&#xff08;即翻页到头时&#xff09;// 配置滚动类型的图例pageTextStyle: {color: &…

EmguCV学习笔记 VB.Net 4.4 图像形态学

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 教程VB.net版本请访问&#xff1a;EmguCV学习笔记 VB.Net 目录-CSDN博客 教程C#版本请访问&#xff1a;EmguCV学习笔记 C# 目录-CSD…

vue3模拟生成并渲染10万条数据,并实现本地数据el-table表格分页

效果图&#xff1a; 一点都不卡 话不多说&#xff0c;直接上码 <template><div class"container"><h3 class"table-title">el表格 分页</h3><el-table :data"tableList.slice((currentPage-1)*pageSize, currentPage*p…

OpenCV与AI深度学习 | 使用OpenCV图像修复技术去除眩光

本文来源公众号“OpenCV与AI深度学习”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;使用OpenCV图像修复技术去除眩光 眩光是一种因过度和不受控制的亮度而引起的视觉感觉。眩光可能会使人丧失能力或只是让人感到不舒服。眩光是一…

源控终端|为么叫源控终端以及SDAF-8860源控终端具备发电单元次/超同步振荡监测监视,告警的功能在新能源场站中的应用

源控终端|为么叫源控终端以及SDAF-8860源控终端具备发电单元次/超同步振荡监测监视,告警的功能在新能源场站中的应用 SDAF-8860源控终端具备发电单元次/超同步振荡监测监视,告警的功能在新能源场站中的应用 SDAF-8608源控终端具有发电单元并网、脱网、低穿等信息快速采集与传输…

数据库;SQL语言; 数据库编程

数据库&#xff1a; 1.概念&#xff1a; 文件&#xff1a;存放数据&#xff0c;掉电数据不会丢失&#xff0c;软件的配置及想要持续保存的数据放入文件中 大批量数据存储和管理时使用数据库 2.类型&#xff1a; 1.关系型数据库 1.Or…

项目启动报错:liquibase.lockservice:? - Waiting for changelog lock....

异常报错&#xff1a; 原因 工作流表部分日志表被锁&#xff0c;可能上次未正常终止程序导致的异常。 处理 登录mysql指定项目对应数据库 SELECT * FROM DATABASECHANGELOGLOCK; UPDATE DATABASECHANGELOGLOCK SET locked0, lockgrantednull, lockedbynull WHERE id1;

通过Qt Creator Plugin开发Qt Creator插件-【金丹篇】

1.前言 由于工作学习需要基于Qt Creator架构开发类似的插件&#xff0c;本人感慨网络上Qt 相关的文档真是少。我直接在官方社区查找Qt Creator Plugin愣是一点资料没有。其实想想也是自定义的三方插件到Qt的IED,主要是个社区的贡献者或官方技术人员自用&#xff0c;他开发布会…

仿OpenAI网页前端制作的ChatGPT,超仿真!!!自定义!!!

仿OpenAI网页前端制作的ChatGPT&#xff0c;超仿真&#xff01;&#xff01;&#xff01;自定义&#xff01;&#xff01;&#xff01; 基于C#和WPF的仿真ChatGPT项目 啊没错我是标题党啊&#xff0c;下面内容AI生成的&#xff0c;主要是介绍我基于C#和WPF制作的仿真ChatGPT项…

【HTTP学习】HTTP协议

HTTP介绍 HTTP请求 这里的get没有请求体&#xff0c;会在网站中直接显示提交的表单。而post提交会将请求参数存放在表单中&#xff0c;需要通过F12进行查看。 HTTP响应 HTTP协议解析

一次不严谨的C++、C、Pascal、Rust等对比

起因 现在ACM用得多的基本上就两种语言&#xff0c;C和Python3&#xff0c;还有部分Java&#xff0c;但是当年ACM必学的Pascal、新近流行的rust也有人用&#xff0c;只不过用户很少。 就以一道codeforce上的算法小题为样本&#xff0c;来对比一样用户数量、执行效率、易写程度…

校园综合服务小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;卖家管理&#xff0c;发布信息管理&#xff0c;订单信息管理&#xff0c;类型管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;发布信息&#xff0…

【PyCharm】从零到一:Python爬虫实战教程,小白、绕过反爬虫机制、实战案例全解析

文章目录 前言一、使用的工具二、安装必要的库三、编写程序1.引入库2.发送GET请求3.绕过反爬虫机制4.解析HTML内容5.输出需要的内容 四、完整源码总结 前言 爬虫&#xff08;Web Crawler&#xff09;是一种自动浏览万维网并从中收集信息的程序。它们常被用于搜索引擎、数据分析…

基于UDP/TCP的 c/s 通信模型

基于UDP c/s通信模型 客户端&#xff08;socket&#xff1b;sendto ...&#xff09; 服务器端 ---把自己的地址公布出去 &#xff08;socket&#xff1b;bind //绑定&#xff1b; recvfrom ...&#xff09; 1.recvfrom函数&#xff1a; ssize_t recvfrom( int sockfd, /…

浮毛难清除、异味难消散?选到不好的宠物空气净化器会有什么危害

近年来&#xff0c;不少人家里都养了宠物&#xff0c;有些是猫、有些是狗&#xff0c;甚至有些是兔子&#xff0c;不少人希望能通过它们抒发心中的郁闷&#xff0c;成为自己的搭子。这些宠物在能带来欢乐的同时也会带来一些小烦恼&#xff0c;比如宠物的浮毛、异味都困扰着我们…

Linux日志管理基本介绍及日志轮替原理

&#x1f600;前言 本篇博文是关于日志管理&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是我的动力&#x1f609;&#x…

ai智能写作生成器哪个好用?推荐五款!

在内容创作日益重要的今天&#xff0c;ai智能写作生成器成为了众多创作者的得力助手。它们不仅能提高写作效率&#xff0c;还能提升写作内容的质量。然而&#xff0c;市场上的ai智能写作生成器众多&#xff0c;选择一个合适的生成器并非易事。本文将为你推荐5款好用的ai智能写作…

[图解]用例规约之业务规则不是算法

1 00:00:01,530 --> 00:00:03,090 像这种某某算法之类的 2 00:00:03,100 --> 00:00:04,130 它往往是什么 3 00:00:05,590 --> 00:00:07,440 某种实现的一个选择 4 00:00:08,140 --> 00:00:09,550 它很可能不是需求 5 00:00:10,620 --> 00:00:13,240 你要问他…