• 2008-02-04

    AntiCheatEngine

    원래는 Cheat Engine 으로도 freeze 를 걸 수 없는 안전한 변수를 만들기 위해서 만든 클래스다. 여기에 CRC라든지 XOR도 넣고, 또 값이 바뀔 때마다 위치도 바꾸고 할려고 했는데, 가만히 생각해보니 속도도 제법 느리고 막상 함수 내에 아래와 같이 로컬 캐시가 있다면 그걸 바꿔버리면 전혀 쓸모 없어져서 그냥 포기버렸다.

    
    float speed = ac.Get<float>("speed");
    // -- inject code here --
    check_next_pos(pos,speed);
    

    어제 주문한 책들이 도착하면 다시 한번 도전해볼까 싶기도 하지만, assembly 쪽 지식이 전무해서...

    
    // AntiCheatEngine.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.
    //
    
    #include "stdafx.h"
    #include <string>
    #include <set>
    #include <vector>
    #include <list>
    #include <hash_map>
    
    #pragma warning(disable:4819)
    #define BOOST_TEST_MAIN
    #include <boost/format.hpp>
    #include <boost/test/unit_test.hpp>
    
    using std::string;
    
    size_t GenerateIndex()
    {
    static size_t key = 0;
    return key++;
    }
    
    class AntiCheat
    {
    typedef std::vector<size_t> INDEX_LIST;
    typedef stdext::hash_map<string, INDEX_LIST > KEY_MAP;
    
    public :
    
    AntiCheat( size_t N = 1024*64 )
    {
    m_Buffer.assign(N,0);
    m_UnusedIndex.assign(N,0);
    std::generate(m_UnusedIndex.begin(),m_UnusedIndex.end(),GenerateIndex);
    std::random_shuffle(m_UnusedIndex.begin(),m_UnusedIndex.end());
    }
    
    bool IsValid() const { return true; }
    
    unsigned int GetCRC() { return m_CRC; }
    
    // FIXME use ASSERT, dont check return value
    template<class T>
    bool Set( const string & name , const T & valueIn )
    {
    KEY_MAP::iterator itr = m_Keys.find(name);
    if ( itr == m_Keys.end() )
    {
    INDEX_LIST idxList;
    for ( size_t i = 0 ; i < sizeof(valueIn); i ++ )
    {
    if ( m_UnusedIndex.empty() )
    return false;
    size_t idx = m_UnusedIndex.back();
    m_UnusedIndex.pop_back();
    idxList.push_back(idx);
    }
    
    itr = m_Keys.insert(make_pair(name,idxList)).first;
    }
    
    INDEX_LIST & indexList = itr->second;
    
    for ( size_t i = 0 ; i < indexList.size() ; i ++ )
    {
    size_t idx = indexList[i];
    m_Buffer[idx] = reinterpret_cast<const char*>(&valueIn)[i];
    }
    
    return true;
    }
    
    template<typename T>
    bool Get( const string & name, T & valueOut )
    {
    KEY_MAP::iterator itr = m_Keys.find(name);
    if ( itr == m_Keys.end() )
    {
    return false;
    }
    
    INDEX_LIST & indexList = itr->second;
    
    for ( size_t i = 0 ; i < indexList.size() ; i ++ )
    {
    size_t idx = indexList[i];
    reinterpret_cast<char*>(&valueOut)[i] = m_Buffer[idx];
    }
    return true;
    }
    
    size_t GetUnusedIndexCount() const { return m_UnusedIndex.size(); }
    
    unsigned int m_CRC;
    
    INDEX_LIST m_UnusedIndex;
    std::vector<unsigned char> m_Buffer;
    KEY_MAP m_Keys;
    };
    
    //////////////////////////////////////////////////////////////////////////
    template<>
    bool AntiCheat::Set<string>( const string & name, const string & valueIn )
    {
    KEY_MAP::iterator itr = m_Keys.find(name);
    if ( itr == m_Keys.end() )
    {
    INDEX_LIST idxList;
    for ( size_t i = 0 ; i < valueIn.size(); i ++ )
    {
    if ( m_UnusedIndex.empty() )
    return false;
    size_t idx = m_UnusedIndex.back();
    m_UnusedIndex.pop_back();
    idxList.push_back(idx);
    }
    
    itr = m_Keys.insert(make_pair(name,idxList)).first;
    }
    
    INDEX_LIST & indexList = itr->second;
    
    for ( size_t i = 0 ; i < indexList.size() ; i ++ )
    {
    size_t idx = indexList[i];
    m_Buffer[idx] = valueIn[i];
    }
    
    return true;
    }
    
    template<>
    bool AntiCheat::Get<string>( const string & name, string & valueOut )
    {
    KEY_MAP::iterator itr = m_Keys.find(name);
    if ( itr == m_Keys.end() )
    {
    return false;
    }
    
    INDEX_LIST & indexList = itr->second;
    
    // FIXME 최초 크기 보다 작은 사이즈는 저장할 수 없다?
    valueOut.assign(indexList.size(),0);
    for ( size_t i = 0 ; i < indexList.size() ; i ++ )
    {
    size_t idx = indexList[i];
    valueOut[i] = m_Buffer[idx];
    }
    return true;
    }
    
    BOOST_AUTO_TEST_CASE( test1 )
    {
    AntiCheat ac(100);
    
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100 );
    
    BOOST_CHECK( ac.IsValid() );
    
    string strIn = "World", strOut;
    
    {
    ac.Set("string",strIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<string>("string",strOut) );
    BOOST_CHECK_EQUAL( strIn, strOut );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size());
    }
    
    {
    int intIn, intOut;
    
    intIn = 1;
    ac.Set("int",intIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<int>("int",intOut) );
    BOOST_CHECK_EQUAL( intIn, intIn );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int));
    
    intIn = -1;
    ac.Set("int",intIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<int>("int",intOut) );
    BOOST_CHECK_EQUAL( intIn, intIn );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int));
    }
    
    {
    bool boolIn, boolOut;
    
    boolIn = true;
    ac.Set("bool",boolIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<bool>("bool",boolOut) );
    BOOST_CHECK_EQUAL( boolIn, boolOut );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int)-sizeof(bool));
    
    boolIn = true;
    ac.Set("bool",boolIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<bool>("bool",boolOut) );
    BOOST_CHECK_EQUAL( boolIn, boolOut );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int)-sizeof(bool));
    }
    
    {
    float floatIn, floatOut;
    
    floatIn = 1.234f;
    ac.Set("float",floatIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<float>("float",floatOut) );
    BOOST_CHECK_EQUAL( floatIn, floatOut );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int)-sizeof(bool)-sizeof(float));
    
    floatIn = -56.78f;
    ac.Set("float",floatIn);
    BOOST_CHECK( ac.IsValid() );
    BOOST_CHECK( ac.Get<float>("float",floatOut) );
    BOOST_CHECK_EQUAL( floatIn, floatOut );
    BOOST_CHECK_EQUAL( ac.GetUnusedIndexCount(), 100-strIn.size()-sizeof(int)-sizeof(bool)-sizeof(float));
    }
    
    }
    
    /*
    
    값을 바꾸면 Key 및 CRC 가 바뀐다
    멤버 함수가 아닌 다른 경로로 값을 바꾸면 CRC 체크가 깨져야 한다.
    AntiCheat 는 하나의 큰 버퍼를 가진다. (ex: 32k)
    여기에 변수를 키값을 이용해서 저장하고 읽어낸다.
    이때 각 변수들은 바이트 단위로 흩어진다. 이때 흩어지는 인덱스는 저장하는 순간 랜덤하게, 중복되지 않게 결정된다.
    (이때문에 unused index set 이 필요함)
    또한 각 변수들은 Verifier 라는 range-checker 와 연동될 수 있다.
    키값 역시 다이나믹하게 결정된다. (가령 player.hp 의 경우 매크로에 의해서 __K(PlayerHP) -> 세션키와의 XOR 등등 )
    Verify()를 호출하면 각 키에 대해서 Verify 한 다음,
    특정 영역 마다 0xEF 등 boundry checker 를 넣어두고, 바뀌었는지 체크한다.
    
    문자열일 경우 (길이 2바이트,본문) 이렇게 저장해야 한다. 만약 길이가 달라질 경우 인덱스를 새로 얻어 오거나 반납해야 한다.
    
    */
    
  • 2008-01-29

    Solid Compression

    learning_reiot.png

    소니군의 가상 파일 시스템 작업을 배후조종 하면서 알아낸 팁.

    여러 파일들을 하나의 스트림으로 압축해버리는 Solid Compression 으로 압축한 파일에서는, 단 하나의 파일을 해제하는데에도 꽤 오랜 시간이 걸린다.

    see also:

  • 2008-01-26

    physfs

    지금까지는 압축 포맷 == 압축 알고리즘이라고 믿어 왔다. 무슨 말이냐 하면, zip 포맷은 zip 압축 알고리즘, rar 은 rar 압축 알고리즘을 쓰니까, 따라서 어떤 압축 알고리즘이라 할지라도 다중 파일, 다중 폴더, 암호화 같은 걸 다 지원한다고만 생각했다는 뜻이다.

    그런데, 이번에 소니군이 physfs 를 조사하는걸 옆에서 껄떡거리면서 깨달은 것은,

    • 압축 알고리즘이란 stream -> stream의 변환일 뿐이다.
    • 즉 파일명이나 폴더명 같은 정보들은 압축 포맷 레벨에서 관리한다.

    physfs 를 처음 알아 봤을 때에는, 라이센스도 누구처럼 GPL도 아니고, 다양한 포맷도 지원하고, 여러 개의 압축 파일에서 asset들을 끄집어낼 수 있어서 패치할 때 유리할거 같아서 잠깐 검토를 해봤는데, 소니군이 소스 코드가 컴파일도 안될 정도로 부실하다길래 그냥 포기해버렸다. ㅋㅋ

  • 2008-01-26

    MSBuild with Cruise Control .Net (3)

    ccnet3

    게임 개발에 있어서 지속적인 통합은, 단순하게 보면 커밋-빌드-테스트-패치-인스톨러가 매번 실행되어야 할 것처럼 보이지만, 실제 업무 프로세스를 감안하면 꼭 그렇지만은 않다.

    패치는 QA팀을 위한 것이다. 즉 실제로 버그가 수정되는 새로운 feature가 들어가든 무언가 테스트할 수 있는 요소가 완결되어야 한다는 의미다. 그러나 과연 개발팀의 커밋이 그러한 완결성을 보장해줄 것인가? 때로는 여러 번 임시적인 커밋을 거쳐야만 하나의 기능이 완료되는 경우가 많다. 원칙적으로는 이런 것은 피해야 하지만, 가끔은 어쩔 수 없이 밀어넣어야 할 때도 있다. 물론 테스트가 잘 갖추어져 있다면 패치와 인스톨러 단계로 들어가기 전에 막아주겠지만, 과연 게임 클라이언트를 위한 자동화된 테스트 슈트를 100% 갖춘 팀이 있을까...

    이런 이유로 결국 패치와 인스톨러 생성을 다른 프로젝트로 분리해버렸다. 아직 인스톨러는 안 붙었고, 패치 쪽도 원래대로라면 asset들과 빌드된 .exe들을 복사해와서 따로 관리해야 하지만 일단 이번 주는 이 정도로 마무리지었다.

    ccnet.config

    • ccnetconfig 라는 GUI 툴을 이용하면 보다 손쉽게 config 파일을 설정할수 있다.
    • BuildPublisher : 패치 프로젝트에서 $(TargetDir)을 패치 폴더로 복사하기 위해 Tasks 섹션에도 넣어보고, PreBuild 에도 넣어봤지만, 둘 다 실행이 안되었다. 그냥 asset 은 svn으로, 실행 파일들은 배치 파일로 복사를 해야 할 듯. (아니면 커스텀 MSBuild 플러그인들을 쓰던가)
    • ScheduleTrigger : 매일밤 자정에 패치와 인스톨러를 빌드하고 싶어서 넣어봤음.
    • Rodemeyer.MsBuildToCCnet.dll 을 logger로 사용할 경우 링크 에러를 만나면 예외가 발생한다. 이 경우 소스 코드를 다운 받은 후, Steven Smith 의 패치를 적용해서 빌드한 후 dll을 업데이트하면 해결된다.

    dashboard.config

    • viewConfigurationProjectPlugin 제거 : 대시보드에서 ccnet.config 파일을 못 읽게 하려면 제거할 것
    • serverLogServerPlugin, serverLogProjectPlugin : 역시나 보안적인 문제가 생길 수 있으니 제거할 것(물론 ccnet.exe 의 디버그 모드를 끄면 되지만)
    • xslReportBuildPlugin 제거 : C++ 프로젝트일 경우 거의 사용되지 않는 NUnit, FxCop, NCover, Simian, Fitness 들도 제거할 것
    • ViewStatisticsReport : 루트 엘리먼트가 없다고 나오는 이유는 ccnet.config 에 StatisticsPublisher가 설정되어 있지 않기 때문이다.

    see also:

  • 2008-01-23

    Upgrade Mantis 1.1.1

    mantis roadmap

    회사에서 Mantis 1.0.8 을 몇 개월 동안 잘 쓰고 있다가, 최근 (Trac에서는 이미 지원하고 있는) Roadmap 기능이 들어간 1.1 버전이 나왔다는 소식을 듣고, 역시나 업그레이드에 도전했다가 한글 문제 때문에 계속 포기해야만 했다. 그러던 오늘 오후, 이번에야말로 기필코 성공하리라고 마음먹고 4시간 넘게 삽질을 했지만 또다시 실패하고 "Fail to upgrade Mantis"라는 제목으로 글을 쓰다가 우연히도 성공해버렸다. 원래는 구차한 실패 과정을 낱낱히 써놨지만, 모두 생략하고 바로 결론으로 넘어가겠다.

    OS : Windows Server 2003 R2
    
    Web Server  : Xampp apache2
    
    DB : Xampp mysql 5.0.45 community
    
    Mantis 1.0.8 + korean_utf8 설정으로 한글 사용중
    
    Encoding : latin1 + latin1_general_ci```
    [기본적인 팁](http://www.mantisbt.org/wiki/doku.php/mantisbt:upgrade_to_utf8)을 읽고 다양한 시도를 거쳐서 실패했는데 결과적으로 가장 핵심은 [이 문서](http://textsnippets.com/posts/show/84)에 들어 있었다. 나의 경우 굳이 [iconv](http://www.gnu.org/software/libiconv/)를 통하지 않고서도 한글 보존에 성공했다.
    

    mysqldump -u username -p bugtracker --default-character-set=latin1 > dump.sql

    SQL문의 모든 latin1 을 utf8 로 변경

    mysql -u username -p --execute="create database bugtracker2 default character set utf8;"

    mysql -u username -p bugtracker2 --default-character-set=utf8 < dump.sql```
    업그레이드에 성공한 것도 무지 기쁘지만, vim 에서 utf-8 포맷의 문서를 자동으로 읽어 들이고 변환할 수 있다는 것을 알게 되어서 더욱 기쁘다. enc, tenc, fenc, fencs 옵션을 잊지 말자.