ProtoBuf生成EmmyLua注解API提示文件(支持复杂的嵌套结构)

身为一个码农,写代码没有提示是最难受最影响效率的吧,偏偏lua就是这样的。目前,大多数的Unity游戏开发者都已经开始使用IntelliJ IDEA来写lua代码,很重要的一个原因就是IDEA中的EmmyLua插件(EmmyLua插件下载地址:https://emmylua.github.io/),使用这个插件可以极大的提高我们的开发效率。

我们自己写的业务逻辑添加emmylua注解很简单,但是如何在项目中生成带注解的proto提高我们的开发效率呢?这时我们就需要一个像导表工具一样的根据proto导出emmylua注解文件的工具。

写工具的时候我想到了两种使用方式:

1、用lua代码来写,然后通过bat来执行lua脚本

2、用C#来实现,写到项目统一的工具类里,方便其他人维护跟移植项目

基于我们项目的需求,我选择了方法2来实现。首先放上效果图:

测试用的proto:

import "commons.proto";
package com.gy.server.packet;

option java_package = "com.gy.server.packet";
option java_outer_classname = "PbActivity";

// -------------------------------测试注释-------------------------------

//战斗状态
enum BattleStatus {
    NO_START = 1;                            //未开启
    BATTLE = 2;                              //战斗状态,允许玩家自由争夺位置
    SETTLEMENT = 3;                          //结算状态,做一系列结算操作,不允许玩家战斗
    RESET = 4;                               //重置状态
    SLEEP = 5;                               //休眠状态,只有控制器能唤醒
    CHANGE = 6;                              //切换状态
}

//这是一段ActivityInfo 前置测试注释1
message ActivityInfo {//这是一段ActivityInfo 前置测试注释2
    optional ActivityModule module = 1;
    optional ActivityData data = 2;
    optional int64 startTime = 3; //活动开启时间
    optional int64 endTime = 4; //活动结束时间
    //这是一个注释
    optional BattleStatus battleStatus = 5; //枚举测试


}//这是一段ActivityInfo 后置测试注释


// ***************************测试注释***************************


message ActivityModule {
    //这是一段测试注释
    optional int32 type = 1;//类型
    optional int32 activityId = 2;
    optional TaskActivityModule task = 10;
}

message ActivityData {
    optional TaskActivityData tasks = 8;                                //任务活动
}


//任务活动
message TaskActivityModule {
    repeated int32 receivedIds = 1; //已领取的奖励ID集合
    repeated TaskActivity tasks = 2; //任务进度
    repeated int32 receivedTaskIds = 3; //已接取任务ID集合

    message TaskActivity {
        optional int32 id = 1; //条目ID
        optional int64 curProgress = 2; //当前进度
        optional int64 totalProgress = 3; //总进度
    }
}

//任务活动数据
message TaskActivityData {
    repeated TaskActivityItem items = 1;

    message TaskActivityItem {
        optional int32 id = 1;
        optional int32 goalId = 2; //目标ID
        repeated PbReward rewards = 3; //常规奖励
        optional int32 vipLimit = 4; //vip等级限制
        optional string jump = 5; //跳转数据
        optional bool isSelectReward = 6; //是否选择奖励
        optional int32 resetFrequency = 7;      //重置频率,1每日,2每周,3每月
    }
}

生成的注解文件:

--  -------------------------------测试注释-------------------------------
-- 战斗状态
---@class BattleStatus : nil
BattleStatus= {
NO_START = "NO_START"; --未开启 
BATTLE = "BATTLE"; --战斗状态,允许玩家自由争夺位置 
SETTLEMENT = "SETTLEMENT"; --结算状态,做一系列结算操作,不允许玩家战斗 
RESET = "RESET"; --重置状态 
SLEEP = "SLEEP"; --休眠状态,只有控制器能唤醒 
CHANGE = "CHANGE"; --切换状态 
}

-- 这是一段ActivityInfo 前置测试注释1
-- 这是一段ActivityInfo 前置测试注释2
---@class ActivityInfo : nil
---@field public module ActivityModule
---@field public data ActivityData
---@field public startTime number@-- 活动开启时间
---@field public endTime number@-- 活动结束时间
-- 这是一个注释
---@field public battleStatus BattleStatus@-- 枚举测试
 
 
local ActivityInfo = {}

--  ***************************测试注释***************************
---@class ActivityModule : nil
-- 这是一段测试注释
---@field public type number@-- 类型
---@field public activityId number
---@field public task TaskActivityModule
local ActivityModule = {}

---@class ActivityData : nil
---@field public tasks TaskActivityData@-- 任务活动
local ActivityData = {}

-- 任务活动
---@class TaskActivityModule_TaskActivity : nil
---@field public id number@-- 条目ID
---@field public curProgress number@-- 当前进度
---@field public totalProgress number@-- 总进度
local TaskActivityModule_TaskActivity = {}

---@class TaskActivityModule : nil
---@field public receivedIds number[]@-- 已领取的奖励ID集合
---@field public tasks TaskActivityModule_TaskActivity[]@-- 任务进度
---@field public receivedTaskIds number[]@-- 已接取任务ID集合
  
local TaskActivityModule = {}

-- 任务活动数据
---@class TaskActivityData_TaskActivityItem : nil
---@field public id number
---@field public goalId number@-- 目标ID
---@field public rewards PbReward[]@-- 常规奖励
---@field public vipLimit number@-- vip等级限制
---@field public jump string@-- 跳转数据
---@field public isSelectReward boolean@-- 是否选择奖励
---@field public resetFrequency number@-- 重置频率,1每日,2每周,3每月
local TaskActivityData_TaskActivityItem = {}

---@class TaskActivityData : nil
---@field public items TaskActivityData_TaskActivityItem[]
local TaskActivityData = {}

话不多说,直接把源码贴出来,该源码经过反复验证、测试,完全适用于各类复杂的PB结构:

using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public class PbUtils
{
    public static string PB_PROTOBUFF_FOLDER_PATH_KEY = "PB_PROTOBUFF_FOLDER_PATH_KEY";

    private static bool isCurEnum = false;
    private static List<string> curClsName = new List<string>();


    [MenuItem("Assets/PB生成lua %&i", false, 200)]
    public static void UpdatePBFromSVN()
    {
        string savedPath = EditorUserSettings.GetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY);
        if (string.IsNullOrEmpty(savedPath))
            SetProtoBufPath();
        GenPbAPI();
    }
    
    //记录当前"程序文件/protobuf"的地址
    private static void SetProtoBufPath()
    {
        //protobuf文件夹的存放路径,根据需求来更改
        string defaultPbPath = Application.dataPath + "../../../../../程序文件/protobuf";
        string path = "";
        //如果默认地址存在,则自动保存默认地址为protobuf地址
        if (FileUtils.ExistsDirectory(defaultPbPath))
        {
            path = defaultPbPath;
        }
        else
        {
            path = EditorUtility.OpenFolderPanel("protobuf文件夹所在的本地目录", defaultPbPath, "");
        }
        if (!string.IsNullOrEmpty(path))
        {
            EditorUserSettings.SetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY, path);
            Debug.Log("protobuf Path: " + path);
        }
    }

    static void GenPbAPI()
    {
        string[] paths = FileUtils.GetFiles(EditorUserSettings.GetConfigValue(PB_PROTOBUFF_FOLDER_PATH_KEY ), "*.proto");
        string emPbDirecPath = Application.dataPath + "..\\..\\EmmyluaAPI\\Pb";
        if (!FileUtils.ExistsDirectory(emPbDirecPath))
        {
            FileUtils.CreateDirectory(emPbDirecPath);
        }        
        List<string> list = new List<string>();
        List<string> finnalLines = new List<string>();

        for (var i = 0; i < paths.Length; i++)
        {
            isCurEnum = false;
            curClsName.Clear();
            list.Clear();
            finnalLines.Clear();
            //当前的proto文件
            string[] lines = File.ReadAllLines(paths[i]);
            SplitLineList2Normal(lines , ref finnalLines);
            for (var j = 0; j < finnalLines.Count; j++)
            {
                string finalLineStr = ExportPbToLuaStr(finnalLines[j]);
                if (!finalLineStr.Equals(string.Empty))
                {
                    list.Add(finalLineStr);
                }
            }

            FileInfo info = new FileInfo(paths[i]);
            string fileName = info.Name.Replace(".proto", ".lua");
            string path = emPbDirecPath + "\\" + fileName;
            //是否只有主干项目才生成PB的注解
//            if (getIsMain)
//            {
                File.WriteAllLines(path, list.ToArray());                
//            }
        }
    }

    static void SplitLineList2Normal(string[] lineList , ref List<string> finnalLines)
    {
        string curContentTemp = String.Empty;
        Dictionary<int ,List<string>> cacheLinesDic = new Dictionary<int, List<string>>();
        Dictionary<int ,List<string>> finalLinesDic = new Dictionary<int, List<string>>();
        Dictionary<string ,string> replaceRuleDic = new Dictionary<string ,string>();
        string curClsName = string.Empty;
        int curClsIndex = 0;
        int totalClsCount = 0;
        for (int index = 0; index < lineList.Length; index++)
        {
            curContentTemp = lineList[index];
            curContentTemp = curContentTemp.Replace("\t", " ");
            if (curContentTemp.Contains("{"))
            {
                curClsIndex = curClsIndex + 1;
                //是否要替换内部类的名字
                string clsName = string.Empty;
                int msgTitleIndex = 0;
                if (curContentTemp.IndexOf("message") >= 0)
                {
                    msgTitleIndex = curContentTemp.IndexOf("message");
                    clsName = curContentTemp.Substring(msgTitleIndex + 7, curContentTemp.IndexOf("{") - msgTitleIndex - 7 ).Trim();
                }
                else if(curContentTemp.IndexOf("enum") >= 0)
                {
                    msgTitleIndex = curContentTemp.IndexOf("enum");
                    clsName = curContentTemp.Substring(msgTitleIndex + 4, curContentTemp.IndexOf("{") - msgTitleIndex - 4 ).Trim();
                }
                if (curClsIndex == 1)
                {
                    //缓存当前message或者enum的名字
                    curClsName = clsName;
                    replaceRuleDic.Clear();
                    cacheLinesDic.Clear();
                    finalLinesDic.Clear();
                }
                else
                {
                    if (!string.IsNullOrEmpty(curClsName))
                    {
                        string nameNew = curClsName + "_" + clsName;
                        curContentTemp = curContentTemp.Replace(clsName , nameNew);   
                        //缓存替换规则
                        if (replaceRuleDic.ContainsKey(clsName))
                            replaceRuleDic[clsName] = nameNew;
                        else
                            replaceRuleDic.Add(clsName , nameNew);
                        //当前类的缓存里是否有需要替换的类名
                        foreach (KeyValuePair<int ,List<string>> item in cacheLinesDic)
                        {
                            for (int itemIndex = 0; itemIndex < item.Value.Count; itemIndex++)
                            {
                                string tempValueStr = item.Value[itemIndex];
                                string[] valueStrList = tempValueStr.Split(' ');
                                bool isContains = false;
                                string ruleKey = string.Empty;
                                string ruleValue= string.Empty;
                                foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
                                {
                                    ruleKey = itemRule.Key;
                                    ruleValue = itemRule.Value;
                                    for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
                                    {
                                        if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
                                        {
                                            valueStrList[ruleIndex] = ruleValue;
                                            isContains = true;
                                            break;
                                        }
                                    }
                                    if (isContains)
                                        break;
                                }
                                //拼接回来字符串
                                tempValueStr = string.Empty;
                                for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
                                {
                                    tempValueStr += valueStrList[strIndex] + " ";
                                }
                                item.Value[itemIndex] = tempValueStr;
                            }
                        }
                        foreach (KeyValuePair<int ,List<string>> item in finalLinesDic)
                        {
                            for (int itemIndex = 0; itemIndex < item.Value.Count; itemIndex++)
                            {
                                string tempValueStr = item.Value[itemIndex];
                                string[] valueStrList = tempValueStr.Split(' ');
                                bool isContains = false;
                                string ruleKey = string.Empty;
                                string ruleValue= string.Empty;
                                foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
                                {
                                    ruleKey = itemRule.Key;
                                    ruleValue = itemRule.Value;
                                    for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
                                    {
                                        if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
                                        {
                                            valueStrList[ruleIndex] = ruleValue;
                                            isContains = true;
                                            break;
                                        }
                                    }
                                    if (isContains)
                                        break;
                                }
                                //拼接回来字符串
                                tempValueStr = string.Empty;
                                for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
                                {
                                    tempValueStr += valueStrList[strIndex] + " ";
                                }
                                item.Value[itemIndex] = tempValueStr;
                            }
                        }
                    }
                }
                List<string> temp = new List<string>();
                temp.Add(curContentTemp);
                cacheLinesDic.Add(curClsIndex , temp);
            }
            else if (curContentTemp.Contains("}"))
            {
                cacheLinesDic[curClsIndex].Add(curContentTemp);
                totalClsCount += 1;
                finalLinesDic[totalClsCount] = cacheLinesDic[curClsIndex];
                cacheLinesDic.Remove(curClsIndex);
                curClsIndex = curClsIndex - 1;
                if (curClsIndex == 0)
                {
                    foreach (KeyValuePair<int, List<string>> item in finalLinesDic)
                    {
                        List<string> temp = item.Value;
                        for (int cacheIndex = 0; cacheIndex < temp.Count ; cacheIndex++)
                        {
                            finnalLines.Add(temp[cacheIndex]);                    
                        }
                        item.Value.Clear();
                    }
                    finalLinesDic.Clear();
                    totalClsCount = 0;
                }
            }
            else
            {
                if (curClsIndex == 0)
                {
                    finnalLines.Add(curContentTemp);                    
                }
                else
                {
                    //是否要替换类名
                    string[] valueStrList = curContentTemp.Split(' ');
                    bool isContains = false;
                    string ruleKey = string.Empty;
                    string ruleValue= string.Empty;
                    foreach (KeyValuePair<string, string> itemRule in replaceRuleDic)
                    {
                        ruleKey = itemRule.Key;
                        ruleValue = itemRule.Value;
                        for (int ruleIndex = 0; ruleIndex < valueStrList.Length; ruleIndex++)
                        {
                            if (valueStrList[ruleIndex].CompareTo(ruleKey) == 0)
                            {
                                valueStrList[ruleIndex] = ruleValue;
                                isContains = true;
                                break;
                            }
                        }
                        if (isContains)
                            break;
                    }
                    //拼接回来字符串
                    curContentTemp = string.Empty;
                    for (int strIndex = 0; strIndex < valueStrList.Length; strIndex++)
                    {
                        curContentTemp += valueStrList[strIndex] + " ";
                    }
                    cacheLinesDic[curClsIndex].Add(curContentTemp);
                }
            }
        }
    }
    
    static string ExportPbToLuaStr(string str)
    {
        if (string.IsNullOrEmpty(str))
            return string.Empty;
        if (str.Contains("import")
            || str.Contains("package com.gy.server.packet;")
            || str.Contains("option java_package")
            || str.Contains("option java_outer_classname ")
        )
        {
            return string.Empty;
        }
        if (str.Contains("{"))
        {
            if (str.Trim().IndexOf("//") == 0)
            {
                str = str.Trim().Replace("//", "-- ");
                return str;
            }
            //开始组建结构
            //是message还是enum
            bool isMsg = str.Contains("message");
            bool isEnum = str.Contains("enum");
            if (isMsg || isEnum)
            {
                string curNote = String.Empty;
                if (str.Contains("//"))
                {
                    //截取服务器添加的注解
                    curNote = str.Substring(str.IndexOf("//") + 2);
                    str = str.Substring(0, str.IndexOf("//")); //向后截取没
                }

                string clsName = string.Empty;
                int msgTitleIndex = 0;
                if (isMsg)
                {
                    msgTitleIndex = str.IndexOf("message");
                    clsName = str.Substring(msgTitleIndex + 7, str.IndexOf("{") - msgTitleIndex - 7 ).Trim();
                }
                else
                {
                    msgTitleIndex = str.IndexOf("enum");
                    clsName = str.Substring(msgTitleIndex + 4, str.IndexOf("{") - msgTitleIndex - 4 ).Trim();
                }
                if (!string.IsNullOrEmpty(curNote))
                {
                    str ="-- " + curNote+"\n" + str;
                }
                if (isMsg)
                {
                    str = str.Replace("message", "---@class").Trim();
                    str = str.Replace("{", ": nil").Trim();
                    isCurEnum = false;
                }
                else
                {
                    string newEnumName = clsName;
                    if (curClsName.Count > 0)
                        newEnumName = curClsName[0] + newEnumName;      
                    str = "---@class " + newEnumName + " : nil\n" + newEnumName +"= {";
                    isCurEnum = true;
                }
                curClsName.Add(clsName);
            }
        }
        else if (str.Contains("}"))
        {
            if (str.Trim().IndexOf("//") == 0)
            {
                str = str.Trim().Replace("//", "-- ");
                return str;
            }
            //组建结构结束
            if (!isCurEnum)
            {
                string nameTemp = curClsName[curClsName.Count - 1];
                str = "local " + nameTemp + " = {}";
            }
            curClsName.RemoveAt(curClsName.Count - 1);
            if (str.Contains("//"))
                str = str.Replace("//", "-- ").Trim();
            str += "\n";
            isCurEnum = false;
        }
        else
        {
            if (isCurEnum)
            {
                PbEnumContent2LuaStr(ref str);
            }
            else
            {
                PbMsgContent2LuaStr(ref str);
            }
        }
        return str;
    }

    //message内容生成注解
    static void PbMsgContent2LuaStr(ref string str)
    {
        int indexTemp = str.Trim().IndexOf("//");
        string curNote = string.Empty;
        if (indexTemp >= 0)
        {
            curNote =  str.Trim().Substring(indexTemp + 2);
            if (indexTemp == 0)
            {
                str = "-- " + curNote;
                return;
            }
        }
        string[] arrSplit = str.Split(' ');
        int index = 0;
        int trueIndex = 0;
        bool isArr = false;
        for (var i = 0; i < arrSplit.Length; i++)
        {
            string sp = arrSplit[i];
            index = index + 1;
//                    if (sp == "\t\trequired" 
//                        || sp == "\t\toptional"
//                    )
            if (sp.Contains("required")
                || sp.Contains("optional")
            )
            {
                trueIndex = index;
                break;
            }
            else if (sp.Contains("repeated"))
            {
                trueIndex = index;
                isArr = true;
                break;
            }
        }
        string fieldType = arrSplit[trueIndex];
        if (fieldType == "int32"
            || fieldType == "int64"
            || fieldType == "float"
            || fieldType == "double"
            || fieldType == "uint32"
            || fieldType == "uint64"
            || fieldType == "sint64"
            || fieldType == "fixed32"
            || fieldType == "fixed64"
            || fieldType == "sfixde32"
            || fieldType == "sfixde64"
        )
        {
            fieldType = "number";
        }
        else if (fieldType == "bool")
        {
            fieldType = "boolean";
        }
        else if (fieldType == "bytes")
        {
            fieldType = "string";
        }

        if (isArr)
        {
            fieldType += "[]";
        }
        string field = null;
        for (var i = trueIndex + 1; i < arrSplit.Length; i++)
        {
            if (arrSplit[i] != string.Empty)
            {
                field = arrSplit[i];
                break;
            }
        }

        if (!string.IsNullOrEmpty(field) && field != " ")
        {
            str = string.Format("---@field public {0} {1}", field, fieldType);
            if (!string.IsNullOrEmpty(curNote))
            {
                str += "@-- " + curNote;
            }
        }
    }
    
    //枚举内容生成注解
    static void PbEnumContent2LuaStr(ref string str)
    {
        if (string.IsNullOrEmpty(str.Trim()))
        {
            return;
        }
        if (str.Trim().IndexOf("//") == 0)
        {
            str = str.Trim().Replace("//", "-- ");
            return;
        }
        string curEnumName = str.Substring(0 ,str.IndexOf("=")).Trim();
        int indexTemp = str.IndexOf("//");
        if (indexTemp > 0)
        {
            string curNote =  str.Substring(indexTemp + 2);
            str = curEnumName + " = \"" +curEnumName + "\"; --" + curNote;
        }
        else if( indexTemp == 0)
        {
            str = str.Replace("//", "-- ").Trim();
        }
        else
        {
            str = curEnumName + " = \"" +curEnumName + "\";";
        }
    }

}

把该脚本放到unity的Editor下,点击
pb导出lua注解文件.png

即可。
发现网上没有什么太多的ProtoBuf生成EmmyLua注解文件的代码,现有的几个也都是bug频出,兼容很差,故将我写的这一套代码贴出来了。

有需要的小伙伴可以自取哦。码字不易,记得给我点赞哟,你们的赞就是我坚持写博客的动力~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342