Git Product home page Git Product logo

game-helper's Introduction

介绍

这是在2018年2-3月份学习windows核心编程等相关技术编写的游戏辅助。本仓库旨在介绍游戏辅助与计算机专业课程的种种联系,了解游戏辅助的工作流程以及开发方法。我当时在某个寒假的时候花了一个月学习这方面的技术,感觉很大程度的提升了我的windows编程,以及操作系统,计算机网络,汇编语言等多方面的水平(对后面的专业课打了不错的基础),所以打算将这个东西分享出来,只是作为学习(没有作为商业用途),里面的代码有很多中文函数和变量(业界流行用中文),方便阅读,现在也没有做这方面的研究了。

这方面涉及到的技术:

  • windows API Hook :主要是应用层API Hook,如hook send发包函数;如果是做游戏保护方面,需要懂驱动层Hook,例如SSDT,Shadow SSDT,IDT Hook等等。 这方面很重要,很多windows应用程序就是靠API Hook工作的,例如杀毒软件,病毒,以及游戏保护系统
  • windows 模块注入 :将你编译的dll模块注入到游戏应用程序中,这方面的技术很多,有修改PE文件的方法,远程线程注入(一般游戏在Hook CreateRemoteThread函数,所以注入失败),劫持注入,输入法注入等。比较容易使用的是劫持注入和输入法注入。
  • 软件逆向 : 使用olldgb等调试器调试游戏会得到汇编代码,一般需要动态调试游戏,在某些关键的语句中下断点,理清游戏的逻辑,这方面比较的困难(不过一般的计算机专业学生都有一定的汇编能力),可以说所以的功能都从这里来的。
  • 熟悉 windows 编程中常用的api,以及懂得如何编译dll(对编译重定位链接这些流程要熟悉)。这方面可以看《windows核心编程》这本书,一开始感觉有点难以编写,后面就慢慢习惯了。。

游戏辅助的3种方法:

  • 指定地址内存值修改(例如 *(int*)(0x1234567) = 12345 )
  • 指定地址函数call调用(汇编实现)
  • 发包收包(Hook send)

仓库的代码结构

  • DNFHelper :编译出dll的源码,是功能实现的核心
  • InjectDll :将dll注入到exe进程的实现(使用输入法注入),是一个简单的MFC桌面应用
  • HelperStart :在无图形界面下将dll注入到exe进程里,并做一些简单的处理。

功能概况

当时实现了很多功能,留了几张截图

  • 召唤人偶

    用人偶来帮你打伤害

    20180203210201

  • 面板(攻击力)提升

    例如修改自身的力量,把自身物攻提到21w,开启这个功能很容易造成游戏数据异常。。

  • 召唤高达

    这个有点像是个人偶,不过人物可以坐在里面操控,攻击力很高

  • 自动攻击 这个是最常用的,让怪物附近自动生成技能然后攻击怪物,攻击的血量是可以指定的,如果血量足够高就是全屏秒杀了。

    F7DEE10C-83FE-406E-8954-58A366E8FF49-520-00000046E47E4ADE_tmp

  • 人物透明,3S评分

    IMG_0300

  • 加buff 可以给自己加大量的buff让自己变得更强

    IMG_0318

  • 自动过图,自动做任务,自动捡物,聚怪,符咒等等

    可以做成一整套全自动的系统,自动升级,自动搬砖,没疲劳了自动换号等。这里录了一个1分钟的视频放在了根目录下(由于用手机录的视频有点大,压缩成一个mp4格式的,不到3MB)

相关知识解答

由于现在windows下的游戏绝大多数都是x86的,所以下面只讨论32位方面的问题,在64位下会不太一样。

1. 什么是基址?

这个概念是最常被提起的,也是最重要的。所谓基址,就是程序中某些变量的地址,这些地址是恒定不变的(在游戏被更新之前)。为什么是基址不会改变呢?可以运行下面这个简单的实例,

#include <stdio.h>

int a = 123;

int main() {
	printf("%X", &a);	
} 

多次运行之后你会发现,输出的值都是不变的,因为a的地址是不变的(即使将 int a = 123 写到函数体内,输出的值也是恒定的)。

  • int a = 123 写在main外部:a是存储在静态存储区的(.data段),在程序编译阶段已经就确定了存储的位置(即段内偏移量是不会变的,除非程序改动后重新编译)
  • int a = 123 写在main里面,a的地址不变的原因:a存储在栈区,栈指针esp在编译阶段已经可以确定是如何移动的(esp初始位置也是确定的),因此a的地址不变(这里只考虑单线程程序,多线程由于有独立栈空间,输出的结果会有变化)

游戏里面的基址通常是一个指针,通过基址+偏移量的方法,定位出关键数据的位置,举个例子

#include <stdio.h>

struct people {
	char* name;
	int age;
	int power; //攻击力	
    // ...
};
 
people *instance; // 人物指针

int main() {
	instance = (people*)malloc(sizeof(people));
	// ... 后面对任务进行初始化等 
} 

由于基址是不变的,所以instance这个变量的地址其实在编译阶段就已经确定了,假设我们通过某种方法获取到instance的地址0x12345678。我们可以通过 *(int*)(0x12345678) 获取到people实际的存储地址,如果我们想要修改人物的攻击力,可以通过*(int*)(*(int*)(0x12345678) + 8) = 12345678 这里8为人物攻击力的偏移量,得到该地址后取指针就可以得到人物攻击力的真实存储地址了。

游戏中的结构体一般都比较复杂,有多级偏移,一般就是 [[[[基址]+偏移1]+偏移2]+偏移3] ... 这种模式([]为去变量的地址)

如果想了解更多,可以去看虚拟地址,操作系统的分段和分页机制,加深对C语言的指针的理解

2. 什么是模块注入?

简单理解:就是在正常运行的程序中注入你编写的模块(dll后缀名),然后让正常运行的程序执行你模块中的代码。通常模块注入后会在模块入口中创建一个后台的工作线程,线程可以共享进程内的全局变量,而且分配和读写内存也不会受到种种限制。

模块注入成功是一切的基础。模块注入的方法很多,大家在网上可以寻找相关的资料学习。我在仓库中提供了我比较常用的输入法注入的源码,稳定性还可以。

远程线程注入:进程A对进程B创建一个线程(进程A拥有权限的情况下),然后在线程调用了LoadLibrary函数,使得模块被加载(这里分享一段远程线程注入的代码,核心函数RemoteInject)

#include <Windows.h>
#include <TlHelp32.h>
#include <tchar.h>
#include <stdio.h>
#include <winsock.h>
#include <string.h>
#include <io.h>
#pragma comment(lib, "ws2_32.lib")

//将单字节char*转化为宽字节wchar_t*  
wchar_t* AnsiToUnicode(const char* szStr)
{
	int nLen = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szStr, -1, NULL, 0);
	if (nLen == 0)
	{
		return NULL;
	}
	wchar_t* pResult = new wchar_t[nLen];
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szStr, -1, pResult, nLen);
	return pResult;
}

//将宽字节wchar_t*转化为单字节char*  
inline char* UnicodeToAnsi(const wchar_t* szStr)
{
	int nLen = WideCharToMultiByte(CP_ACP, 0, szStr, -1, NULL, 0, NULL, NULL);
	if (nLen == 0)
	{
		return NULL;
	}
	char* pResult = new char[nLen];
	WideCharToMultiByte(CP_ACP, 0, szStr, -1, pResult, nLen, NULL, NULL);
	return pResult;
}


BOOL Is64BitOS()
{
	typedef VOID(WINAPI *LPFN_GetNativeSystemInfo)(__out LPSYSTEM_INFO lpSystemInfo);
	LPFN_GetNativeSystemInfo fnGetNativeSystemInfo = (LPFN_GetNativeSystemInfo)GetProcAddress(GetModuleHandleW(L"kernel32"), "GetNativeSystemInfo");
	if (fnGetNativeSystemInfo)
	{
		SYSTEM_INFO stInfo = { 0 };
		fnGetNativeSystemInfo(&stInfo);
		if (stInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64
			|| stInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64)
		{
			return TRUE;
		}
	}
	return FALSE;
}

BOOL Is64BitPorcess(HANDLE hProcess)
{
	if (!Is64BitOS())
		return FALSE;
	else
	{
		if (hProcess)
		{
			typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL);
			LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandleW(L"kernel32"), "IsWow64Process");
			if (NULL != fnIsWow64Process)
			{
				BOOL bIsWow64 = FALSE;
				fnIsWow64Process(hProcess, &bIsWow64);
				if (bIsWow64)
					return FALSE;
				else
					return TRUE;
			}
		}
	}
	return FALSE;
}

void EnableDebugPriv()
{
	HANDLE hToken;
	LUID luid;
	TOKEN_PRIVILEGES tkp;

	OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);

	LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);

	tkp.PrivilegeCount = 1;
	tkp.Privileges[0].Luid = luid;
	tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

	AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);

	CloseHandle(hToken);
}

VOID EnumProcess()
{
	EnableDebugPriv();
	DWORD pid = 0;
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	PROCESSENTRY32 process;
	ZeroMemory(&process, sizeof(process));
	process.dwSize = sizeof(process);
	printf("            进程名称   进程ID    父进程ID  32/64位\n");
	if (Process32First(snapshot, &process))
	{
		do
		{
			printf("%20.20ls    %5d       %5d  ", process.szExeFile, process.th32ProcessID, process.th32ParentProcessID);
			HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, process.th32ProcessID);
			if (Is64BitPorcess(hProcess))
				printf("   64位\n");
			else
				printf("   32位\n");
			CloseHandle(hProcess);
		} while (Process32Next(snapshot, &process));
	}
	CloseHandle(snapshot);
}

HANDLE GetProcessByName(LPCSTR name)
{
	EnableDebugPriv();
	DWORD pid = 0;
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	PROCESSENTRY32 process;
	ZeroMemory(&process, sizeof(process));
	process.dwSize = sizeof(process);
	if (Process32First(snapshot, &process))
	{
		do
		{
			if (stricmp(UnicodeToAnsi(process.szExeFile), name) == 0)
			{
				pid = process.th32ProcessID;
				break;
			}
		} while (Process32Next(snapshot, &process));
	}
	CloseHandle(snapshot);

	if (pid != 0)
	{
		HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
		return hProcess;
	}
	return NULL;

}

HANDLE GetProcessByWindowName(LPWSTR WindowsName)
{
	HANDLE hProcess = NULL;
	DWORD pid, ERRO;
	HWND dnf = ::FindWindow(NULL, WindowsName);
	GetWindowThreadProcessId(dnf, &pid);
	EnableDebugPriv();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hProcess == NULL)
	{
		printf("找不到进程!\n");
		return 0;
	}
	return hProcess;
}

BOOL RemoteInject(HANDLE hProcess, LPCSTR szLibPath)
{
	HANDLE hThread;
	HMODULE modHandle = GetModuleHandle(_T("Kernel32"));
	DWORD dwsize = (strlen(szLibPath) + 1)*sizeof(CHAR);
	LPVOID pLibRemote = VirtualAllocEx(hProcess, NULL, dwsize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	WriteProcessMemory(hProcess, pLibRemote, (LPVOID)szLibPath, dwsize, NULL);
	if (GetLastError()) return FALSE;
	LPTHREAD_START_ROUTINE func = (LPTHREAD_START_ROUTINE)GetProcAddress(modHandle, "LoadLibraryA");
	DWORD tid;
	hThread = CreateRemoteThread(hProcess, NULL, 0,
		func,
		pLibRemote,
		0,
		&tid);
	if (GetLastError()) return FALSE;
	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);
	VirtualFreeEx(hProcess, pLibRemote, dwsize, MEM_DECOMMIT);
	return TRUE;
}

void usage()
{
	printf("功能1:枚举系统中的进程\n");
	printf("inject -e\n\n");
	printf("功能2:向指定进程注入代码\n");
	printf("inject {exe} {dll}\n");
	printf("inject -pid {pid} {dll}\n");
	printf("例如: inject.exe chrome.exe mydll.dll\n");
	printf("注意exe版本和dll版本以及注入的程序的版本要一致\n");
	exit(0);
}

int main(int argc, char* argv[])
{
	CHAR szLibPath[MAX_PATH] = { 0 };
	GetCurrentDirectoryA(MAX_PATH, szLibPath);
	CHAR szDllName[MAX_PATH] = { 0 };
	HANDLE hProcess = NULL;
	if (argc == 2)
	{
		if (strcmp(argv[1], "-e"))
			usage;
		EnumProcess();
		return 0;
	}
	if (argc == 4)
	{
		if (strcmp(argv[1], "-pid"))
			usage();
		else
		{
			DWORD pid = atoi(argv[2]);
			hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
		}
		strcpy(szDllName, argv[3]);
	}
	if (argc == 3)
	{
		strcpy(szDllName, argv[2]);
		hProcess = GetProcessByName(argv[1]);
	}
	if (argc != 2 && argc != 4 && argc != 3) 
		usage();
	if (hProcess == NULL)
	{
		printf("找不到进程!\n");
		return 0;
	}
	BOOL Is64_1 = Is64BitPorcess(hProcess);
	BOOL Is64_2 = Is64BitPorcess(GetCurrentProcess());
	if (Is64_1 != Is64_2)
	{
		if (Is64_1)
			printf("请运行64位版本\n");
		else
			printf("请运行32位版本\n");
		return 0;
	}
	strcpy(szLibPath + strlen(szLibPath), "\\");
	strcpy(szLibPath + strlen(szLibPath), szDllName);
	if (_access(szLibPath, 0))
	{
		printf("%s : dll不存在\n", szLibPath);
		return 0;
	}
	if (!RemoteInject(hProcess, szLibPath))
		printf("注入失败\n");
	else
		printf("注入成功\n");
	return 0;
}

3. 什么是API Hook?

简单理解:假设我现在Hook了A函数,当其他线程调用了A函数,但实际上确是执行了B函数,而调用者却完全不知情。

例如

int A() {
    // 这一个是正常的函数,一般是游戏开发者编写的
	return 1;
}

int B() {
    // 这个是恶意的函数(一般是你模块注入成功后你编写的函数)
	// 通常是先保存A的地址,然后
    return 0;
}

int main() {
    // Hook(A, B); // 在其他线程中,A函数被Hook为B函数  
    int res = A(); // 这里调用者不知情,实际上调用的是B函数
}

分享Hook MessageBox弹窗的例子。

#include <windows.h>

//定义函数原型: MessageBoxA
typedef int WINAPI OLD_MessageBoxA( __in_opt HWND hWnd, __in_opt LPCSTR lpText, __in_opt LPCSTR lpCaption, __in UINT uType);

DWORD addr1; // 保存源码MeesageBoxA的地址

__declspec(naked)   int __stdcall My_MessageBoxA( __in_opt HWND hWnd, __in_opt LPCSTR lpText, __in_opt LPCSTR lpCaption, __in UINT uType)
{
	__asm
	{
			mov edi, edi
			push ebp
			mov ebp ,esp 
	}	

	lpText="已被HOOK";  // 修改数据
	__asm
	{
		mov eax,addr1
		add eax,5 // 前5字节是我们的长jump指令
		jmp eax   // 跳到MessageBox真正的代码区域
	}			
}

int _tmain(int argc, _TCHAR* argv[])
{
	DWORD jump_code=0;
	byte  mye9 = 0xe9;
	HMODULE DLLHANDLE= LoadLibraryA("USER32.DLL");
	addr1= (DWORD)(GetProcAddress(DLLHANDLE,"MessageBoxA"));

	//改变内存读写属性 
	VirtualProtect((LPVOID)addr1,10,PAGE_EXECUTE_READWRITE,NULL);
	 	 
	//修改第一字节
	if (WriteProcessMemory(GetCurrentProcess(),(LPVOID)addr1,&mye9,1,NULL)==0)
	{
		printf("写入HOOK数据失败");
	}

	 //修改后4字节 :跳转码  JMP 1234
	 //跳转码=将要跳到的函数-原来的函数地址-5
	 //我们的跳转码=My_MessageBoxA-addr1-5
	 jump_code=(DWORD)&My_MessageBoxA-addr1-5;
	 if (WriteProcessMemory(GetCurrentProcess(),(DWORD*)(addr1+1),&jump_code,4,NULL)==0)
	 {
		 printf("写入HOOK数据失败");
	 }

	::MessageBoxA(0,"HOOK1","提示",0);
	::MessageBoxA(0,"HOOK2","提示",0);
	::MessageBoxA(0,"HOOK3","提示",0);

	 return 0;
}

运行之后调用MessageBoxA,窗口中的内容都是"已被Hook"。

这种Hook方法称为Inline Hook,因为在windows api中,如果函数被声明为__stdcall的话,汇编后的前5个字节是这样的

mov edi, edi  // 2字节
push ebp   // 1字节
mov ebp, esp // 2字节

这5个字节刚好可以组成一条长jump指令,只要修改函数代码段即可。

4. 参数入栈方式

在windows下参数一般都是从右往左入栈的,下面一个简单的程序

int fun(int a, float b, int c) {
    return 1;
}

int main() {
    ...
    func(1, 2, 3);
}

汇编之后,fun函数通常是这样的。

push        ebp  
mov         ebp,esp  // 保存esp寄存器到ebp中

sub         esp,0C0h  // 开辟栈空间

push        ebx  
push        esi  
push        edi  // 保存寄存器

lea         edi,[ebp-0C0h]  
mov         ecx,30h  
mov         eax,0CCCCCCCCh  
rep stos    dword ptr es:[edi]  // 将栈空间初始化

mov         eax,1  // 将返回值设置1,eax为返回值寄存器

pop         edi  
pop         esi  
pop         ebx  
mov         esp,ebp  // 复原esp寄存器
pop         ebp   // 复原ebp寄存器
ret // 回到调用者的位置,相当于 add esp, 4; jmp [esp-4];  

调用者方汇编代码通常是这样的

push 3
push 2
push 1
call func  // 相当于 push pc; jmp esp
add esp, 0xCh // 因为push了3次,esp降低了12字节,所以需要add回来

想了解更多,可以搜索程序运行时的栈空间布局,例如func函数的第一个变量地址是ebp-8,第二个变量是ebp-12,返回值存储在eax寄存器中

5. 如何实现花里胡哨的特效(例如怪物自动扣血)?

几乎所有的辅助功能,都是调用游戏开发者写的某个函数的,比如怪物受到技能攻击,我们首先需要通过汇编找到攻击的call地址,然后在我们的dll模块中调用这个call从而实现效果,一般使用汇编来实现。

(这里源码中基本都是中文,vs支持中文)

基本流程:通过遍历取得所有怪物的坐标地址和指针,然后用技能call对怪物进行攻击,使得怪物掉血。

void 释放call(int 对象基址, int 代码, int 伤害, int X, int Y, int Z)
{
	_asm
	{
		push Z
		push Y
		push X
		push 伤害
		push 代码
		push 对象基址
		mov eax, 释放Call
		call eax
		add esp, 0x18
	}
}

void 全屏遍历()
{
	int 一级偏移 = read(人物基址);
	int 二级偏移 = read(一级偏移 + 地图偏移);
	if (二级偏移 == 0)
		return;
	int 首地址_ = read(二级偏移 + 首地址);
	int 尾地址_ = read(二级偏移 + 尾地址);
	int 怪物数量 = (尾地址_ - 首地址_) / 4;
	int 技能数量_ = 0;
	int x, y, z;
	if (技能伤害 == 0)
	{
		int sum = 0, cnt = 0;
		for (int i = 1; i <= 怪物数量; i++)
		{
			int 地址 = read(首地址_ + 4 * i);
			if (地址 <= 0)
				continue;
			int 类型 = read(地址 + 类型偏移);
			int 阵营 = read(地址 + 阵营偏移);
			int 血量 = read(地址 + 怪物血量偏移);
			if ((类型 == 529 || 类型 == 545 || 类型 == 273) && 阵营 > 0 && 血量 > 0)
			{
				cnt++;
				sum += 血量;
			}
		}
		if (cnt == 0) return;
		for (int i = 0; i < 技能代码个数; i++)
		{
			超级加密(Atk[i] + 32, sum / cnt );
		}
	}
	for (int i = 0; i <= 怪物数量; i++)
	{
		int 地址 = read(首地址_ + 4 * i);
		if (地址 <= 0)
			continue;
		int 类型 = read(地址 + 类型偏移);
		int 阵营 = read(地址 + 阵营偏移);
		int 血量 = read(地址 + 怪物血量偏移);
		int 代码 = read(地址 + 对象代码);
		x = (int)read<float>(地址 + X坐标);
		y = (int)read<float>(地址 + Y坐标);
		z = (int)read<float>(地址 + Z坐标);
		if ((类型 == 529 || 类型 == 545 || 类型 == 273) &&  阵营 > 0 && 血量 > 0 && 地址 != 一级偏移)
		{
			技能代码 = 技能代码集合[index % 技能代码个数];
			释放call(一级偏移, 技能代码, 0, x, y, z);
			技能数量_ += 1;
			index++;
			if (技能数量_ == 技能个数)
				break;
		}
		
	}
	if (index > 100)
		index = 0;
}

6. 发包相关

一般都是下send断点,然后通过调用堆栈定位关键函数,这方面需要对网络编程和调试水平非常的熟悉,发包可以实现很多功能,甚至是脱机刷图。。。

game-helper's People

Contributors

huanghongkai avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.