吔 ! 发表于 2018-6-26 11:56:22

【Source SDK using C++】使用Source SDK编写Left4Dead2服务器的插件

本帖最后由 吔 ! 于 2020-10-7 13:31 编辑

该帖在此处有最新的更新,去掉了一些迷惑词语与中二片段
【写在前面】
写C++的同志并且想搞这个服务器插件的,我觉得您得看一看这篇文章。
原帖地址:本帖非搬运,原帖可能已经失效,原作者就是我
最后,请不要介意本文屎一样的排版
使用Source SDK编写Source引擎服务器的插件(L4D2 Version)
在此之前,如果还没搞明白metamod,sourcemod,source SDK三者之间联系的,请先看这个帖子(这个帖子反正已经凉了,但里面有些“干货”可以拿来看看)
本篇文章意旨您可以使用C++来编写您服务器的插件,据我所知,2013年之后很少有人再提起C++编写此引擎插件的事情了(现在2018了),取而代之的是metamod与Sourcemod(现在甚至编写metamod上面插件的人都很少了),国外基本也变成这样,既然如此,为什么我还要在此提出C++的使用呢?原因很简单:

[*]自由超多人数服务器的性能
[*]可以以这个插件作为学习C++的铺垫,为插件做一个HelloWorld,同时可以增强自身C++的功底。
[*]pawn语言比较冷门,Source pawn更加不用说了,学精了也只能用在这些特定的地方,而C++可以跨平台干出任何想要的事情。
[*]目测可以与exe一样达到防止轻易被反编译。

为什么自由,知道指针与内嵌汇编的人都知道它的厉害之处,而且需要一个厉害的人去驾驭它,但是这种人的孩子估计都上高中了,这也是C++插件基本绝种的主要原因(看看metamod社区多么冷清),第二,SourcePawn的性能在人数少的时候自然没啥可体现,但人多起来的时候,这个时候非常考验服务器性能(因为不优化插件,你就只能增加服务器性能,你得花钱啊!)。毕竟Sourcemod只是一个接口,如果接口性能考虑不完全,势必导致在接口之上编写的SourcePawn的性能大大降低。
我说了这么多,我并不是讨厌SourcePawn,而是因为SourcePawn限制了我的想象力,如果说把C++看成是宇宙,那么SourcePawn就是地球,这个概念想必都懂,我也就不废话了,进入正题:
想完成从源代码到插件的编写,你需要几样东西:(以下伸手党福利)

[*]visual studio 2008(最好是express版本)
这是什么链接我就不说了,自己拿某雷去下,浏览器下载比较慢的
[*]hl2-sdk-l4d2
可以说是求生2的源代码
[*]求生之路2的scrds服务器
这个的话,反正我是官方正版的,可以去steam客户端的工具里面下载(图个情怀,现在谁还不买正版玩,我给个鄙视)

准备好这些,那就开始吧,准备工作挺简单的:

[*]先用vs2008创建一个项目,你可以不创建在桌面上,但是创建之后别忘记在哪个目录。(我同学经常犯这毛病)
单击【文件】->【新建】->【项目】,确保选择Win32 项目,名称随便输,位置自己定(还是那句话,创建之后别忘记在哪个目录),然后选择空项目(这个很有必要)然后就创建成功了,而且务必选中DLL,不然后期会很难搞,都是点点鼠标的事儿,我就不放图了。
[*]拷贝头文件与库至工程目录
将你的下载的hl2-sdk-l4d2的zip包解包,然后你能看见一大堆玩意儿,【choreoobjects, common, devtools, game, 等等】,然而你只需要其中的三个目录,game, lib, 与public。将这三个文件夹复制到你的工程目录(哈?你说目录不知道是啥?我刚才强调了两遍的话白说了吗?),拷贝完了以后,假如你如果想钻研一下Source的源码的,你可以看看hl2-sdk-l4d2 zip包下的其他目录,不想钻研的,SDK留着也没用了,直接删了便好。
[*]设置工程,这个过程比较麻烦,但是弄好就一劳永逸,涉及编译原理的东西我也不说了,清楚的知道我在做什么,不清楚的你照做便罢。
首先你要为你的工程添加一个源文件,源文件名自己取,最好不要用中文,不这么做而直接跳过做下面这步的话,我已经能想象到你满头问号的表情了,创建完先不用动,看下一步。创建好功成之后的vs2008的主界面的左上角应该有一个树形列表,写着 解决方案“项目名”、“项目名”, 头文件,源文件, 资源文件,右键那个项目名(解决方案下面那个),再点击属性,在弹出的框里你应该能找到c/c++这一个标签(上面一步没有做,这一步你找不到这个标签的),点击它,在右边应该能找到包含附加目录,在里面填写以下内容:public;public\tier0;public\tier1;public\tier2;public\tier3;game;game\shared;game\server;game\client然后展开链接器标签,点击常规,在附加库目录里填上lib\public,再点击输入,分别在附加依赖项和忽略特定库里添加mathlib.lib tier0.lib tier1.lib tier2.lib vstdlib.lib和libc;libcd;libcmt,至此,项目设置应该是完成了,其他东西也没必要动了。
[*]编写源代码,挂接IServerPluginCallbacks接口实现并且暴露自身某块以供服务器调用。
说的很好听,不过就是继承一下IServerPluginCallbacks再写个宏就完事儿了。代码我就放这儿吧。(懒得上传文件)#define GAME_DLL
#include <stdio.h>
#include "interface.h"
#include "filesystem.h"
#include "engine/iserverplugin.h"
#include "game/server/iplayerinfo.h"
#include "eiface.h"
#include "igameevents.h"
#include "convar.h"
#include "Color.h"
#include "vstdlib/random.h"
#include "engine/IEngineTrace.h"
#include "tier2/tier2.h"
#include "shareddefs.h"
#include "tier0/memdbgon.h"

class CEmptyServerPlugin: public IServerPluginCallbacks
{
public:
      CEmptyServerPlugin();
      ~CEmptyServerPlugin();

      // IServerPluginCallbacks methods
      virtual bool                        Load(      CreateInterfaceFn interfaceFactory, CreateInterfaceFn gameServerFactory );
      virtual void                        Unload( void );
      virtual void                        Pause( void );
      virtual void                        UnPause( void );
      virtual const char   *GetPluginDescription( void );      
      virtual void                        LevelInit( char const *pMapName );
      virtual void                        ServerActivate( edict_t *pEdictList, int edictCount, int clientMax );
      virtual void                        GameFrame( bool simulating );
      virtual void                        LevelShutdown( void );
      virtual void                        ClientActive( edict_t *pEntity );
      virtual void                        ClientDisconnect( edict_t *pEntity );
      virtual void                        ClientPutInServer( edict_t *pEntity, char const *playername );
      virtual void                        SetCommandClient( int index );
      virtual void                        ClientSettingsChanged( edict_t *pEdict );
      virtual PLUGIN_RESULT      ClientConnect( bool *bAllowConnect, edict_t *pEntity, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen );
      virtual PLUGIN_RESULT      ClientCommand( edict_t *pEntity, const CCommand &args );
      virtual PLUGIN_RESULT      NetworkIDValidated( const char *pszUserName, const char *pszNetworkID );
      virtual void                        OnQueryCvarValueFinished( QueryCvarCookie_t iCookie, edict_t *pPlayerEntity, EQueryCvarValueStatus eStatus, const char *pCvarName, const char *pCvarValue );

};


//
// The plugin is a static singleton that is exported as an interface
//
CEmptyServerPlugin g_EmtpyServerPlugin;
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CEmptyServerPlugin, IServerPluginCallbacks, INTERFACEVERSION_ISERVERPLUGINCALLBACKS, g_EmtpyServerPlugin );

//---------------------------------------------------------------------------------
// Purpose: constructor/destructor
//---------------------------------------------------------------------------------
CEmptyServerPlugin::CEmptyServerPlugin()
{
}

CEmptyServerPlugin::~CEmptyServerPlugin()
{
}

//---------------------------------------------------------------------------------
// Purpose: called when the plugin is loaded, load the interface we need from the engine
//---------------------------------------------------------------------------------
bool CEmptyServerPlugin::Load(      CreateInterfaceFn interfaceFactory, CreateInterfaceFn gameServerFactory )
{
      return true;
}

//---------------------------------------------------------------------------------
// Purpose: called when the plugin is unloaded (turned off)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::Unload( void )
{
}

//---------------------------------------------------------------------------------
// Purpose: called when the plugin is paused (i.e should stop running but isn't unloaded)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::Pause( void )
{
}

//---------------------------------------------------------------------------------
// Purpose: called when the plugin is unpaused (i.e should start executing again)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::UnPause( void )
{
}

//---------------------------------------------------------------------------------
// Purpose: the name of this plugin, returned in "plugin_print" command
//---------------------------------------------------------------------------------
const char *CEmptyServerPlugin::GetPluginDescription( void )
{
      return "Plugin Made by Yourself";
}

//---------------------------------------------------------------------------------
// Purpose: called on level start
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::LevelInit( char const *pMapName )
{

}

//---------------------------------------------------------------------------------
// Purpose: called on level start, when the server is ready to accept client connections
//                edictCount is the number of entities in the level, clientMax is the max client count
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::ServerActivate( edict_t *pEdictList, int edictCount, int clientMax )
{

}

//---------------------------------------------------------------------------------
// Purpose: called once per server frame, do recurring work here (like checking for timeouts)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::GameFrame( bool simulating )
{
}

//---------------------------------------------------------------------------------
// Purpose: called on level end (as the server is shutting down or going to a new map)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::LevelShutdown( void ) // !!!!this can get called multiple times per map change
{

}

//---------------------------------------------------------------------------------
// Purpose: called when a client spawns into a server (i.e as they begin to play)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::ClientActive( edict_t *pEntity )
{

}

//---------------------------------------------------------------------------------
// Purpose: called when a client leaves a server (or is timed out)
//---------------------------------------------------------------------------------
void CEmptyServerPlugin::ClientDisconnect( edict_t *pEntity )
{
}

void CEmptyServerPlugin::OnQueryCvarValueFinished( QueryCvarCookie_t iCookie,
                                                          edict_t *pPlayerEntity,
                                                          EQueryCvarValueStatus eStatus,
                                                          const char *pCvarName,
                                                          const char *pCvarValue )
{
}

void CEmptyServerPlugin::ClientPutInServer(edict_t* pEntity, const char* playername)
{
}

void CEmptyServerPlugin::SetCommandClient(int index)
{

}

void CEmptyServerPlugin::ClientSettingsChanged(edict_t* pEdict)
{

}

PLUGIN_RESULT CEmptyServerPlugin::ClientConnect(bool* bAllowConnect, edict_t* pEntity, const char* pszName, const char* pszAddress, char* reject, int maxrejectlen)
{
      return PLUGIN_CONTINUE;
}

PLUGIN_RESULT CEmptyServerPlugin::ClientCommand(edict_t* pEntity, const CCommand& args)
{
      return PLUGIN_CONTINUE;
}

PLUGIN_RESULT CEmptyServerPlugin::NetworkIDValidated(const char* pszUserName, const char* pszNetworkID)
{
      return PLUGIN_CONTINUE;
}
这段代码我没有测试过,这是我从其他工程提取出来的一个纯的代码, 代码编译错误请留言,我会尽力帮忙看的。编译好了,进工程目录找到编译好的dll,放到桌面上,以便下一步使用。
[*]【广告推销时间】我有一个写MC插件的教程,我详细介绍了插件的涵义与含义,以及插件是怎么插进服务器的,与MC服务器相同的是,L4D2服务器也有一个插件引导文件,后缀名为vdf,可以新建一个vdf文件(非中文名),然后放到addons文件夹(这个读者应该清楚吧),在vdf里面写上以下内容:"Plugin"
{
    "file"    "这里填写你的插件的目录,如果你dll文件放在addons目录里面,那么这里你可以写addons/文件名(不带.dll),注意,最外边两个引号你得留着"
}这段代码我建议手打,而且最好用notepad++编辑,不然服务器识别不出来就完蛋,而且缩进最好用tab键。(我是基于metamod包里面自带的那个vdf文件修改的)
[*]运行服务器查看插件运行状态
没意外的情况下,服务器应该能运行起来了,这个时候在控制台输入plugin_print,应该可以看到插件了(如果只有两条杠,杠之间啥也没有,那你失败了,重新来过),或者你也可以留言寻求帮助(当然你得问明白),这个只是一个什么也没有做的空插件,我会在这个论坛写一些插件供参考。
[*]尾声
到这儿成功的朋友和我击个掌吧!(挺不容易的),因为这个时候你已经游于自由的海洋之中了,我可以毫不负责任的讲,现在你啥都可以做,你可以在cpp文件里引用windows.h然后为你的插件创建一个窗口,窗口里调用d3d功能然后模拟一个客户端绘制并且监控当前服务器内正在发生画面(这个可以做到,当然我不会做,因为需要算法的知识),还可以与酷Q机器人进行对接,不需要数据库,实现QQ群内输入指令检查当前服务器的人数和所有玩家的名称,能想到并且能做到的事情太多了,还能够把代码交给单片机集群去处理(相信我,能实现),甚至还能够与MC服务器进行数据对接,非常神奇,然而这一切的一切,不过就是从一个小小的HelloWorld与一份浇不灭的信心,相信想钻的读者一定能够成功,最后,自由万岁
页: [1]
查看完整版本: 【Source SDK using C++】使用Source SDK编写Left4Dead2服务器的插件