1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Base class for transactions that modify a CGCSharedObjectCache and the database
//
//=============================================================================
#ifndef SHAREDOBJECTTRANSACTION_H
#define SHAREDOBJECTTRANSACTION_H
#ifdef _WIN32
#pragma once
#endif
#include <functional>
namespace GCSDK
{
template < typename TSharedObject >
struct SharedObjectContainsAuditEntryType
{
typedef char t_Yes[1];
typedef char t_No[2];
template < typename T >
static t_Yes& Test( typename T::CAuditEntry * );
template < typename T >
static t_No& Test( ... );
enum { kValue = sizeof( Test<TSharedObject>( NULL ) ) == sizeof( t_Yes ) };
};
//-----------------------------------------------------------------------------
// Purpose: Let's stop writing transactional code by hand everywhere!
//
// Core usage:
//
// - make a new instance, either starting a new SQL transaction or hooking
// into an existing open transaction.
//
// - call some combination of AddNewObject/RemoveObject to add newly-allocated
// objects or remove existing objects. If the types of objects being added/
// removed contain a linked audit data class, you're required to pass in
// a filled-out instance with the add/remove request.
//
// - modify some existing objects. You can either call ModifyObject directly
// or call one of the helper functions/macros (ie., ModifyObjectSch).
// Whatever changes get made here won't be visible anywhere outside this
// transaction object until a commit succeeds.
//
// - try to do a commit. If this succeeds, everything worked and memory has
// been updated to reflect DB changes (if any). If this fails, or if the
// transaction object gets destroyed with an open transaction, all queued
// SQL work will be dropped and no potential memory changes will happen.
//
// That was too long. What's a short version?:
//
// {
// CSharedObjectTransactionEx transaction( pSomeLockedSOCache, "Sample Tool Transaction" );
// transaction.RemoveObject( pToolItem, CEconItem::CAuditEntry( ... ) );
// transaction.RemoveObject( pToolTargetItem, CEconItem::CAuditEntry( ... ) );
// transaction.AddNewObject( pNewResultItem, CEconItem::CAuditEntry( ... ) );
// transaction.ModifyObjectSch( pEconGameAccount, unNumToolsUsed, pEconGameAccount->Obj().m_pEconGameAccount + 1 );
// Verify( transaction.BYieldingCommit() );
// }
//
// Guarantees this class makes:
//
// - if the initial state is correct and client code isn't malicious, a
// lock on the SO cache passed in will be maintained for the lifetime
// of the class.
//
// - externally, no changes will be visible on the GC or on cients until a
// commit attempt takes place. If the commit failures, no changes will
// take place in memory. If the commit succeeds, all memory changes will
// become visible "simultaneously" (no yields, but not atomic from a
// threading perspective.
//
// - this class will do the minimum amount of work possible to guarantee
// correct behavior. If you don't touch any networked objects, no network
// updates will be sent. If you don't touch any DB-backed objects, no DB
// work will be done.
//-----------------------------------------------------------------------------
class CSharedObjectTransactionEx
{
public:
CSharedObjectTransactionEx( CGCSharedObjectCache *pLockedSOCache, const char *pszTransactionName );
~CSharedObjectTransactionEx();
// AddNewObject:
// Add a new object to this user's SO cache. If the type of this object supports audit entries, you're
// required to pass in a CAuditEntry of the appropriate type (ie., CEconItem::CAuditEntry). If your type
// doesn't support audit entries, and you're really sure that not auditing in the correct behavior, you
// call the single-argument version below. In both cases, it's illegal to pass in an object that's
// a raw CSharedObject pointer because then this code doesn't know what type of audit data to look for.
//
// It's possible to set up these functions using SFINAE so that the compiler will only see the appropriate
// version, but this way produces prettier error messages.
// Add an object to this user's cache, conditional on the SQL transaction succeeding, including writing an
// audit entry to SQL explaining where this object came from.
template < typename TSharedObject >
void AddNewObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit )
{
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) );
COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::kValue );
m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject );
m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject );
}
// Add an object to this user's cache, conditional on the SQL transaction succeeding, but don't write any
// audit data. In general, this is probably the wrong thing to do and you want to be making sure this object
// type supports writing audit data and then writing it.
template < typename TSharedObject >
void AddNewObject( TSharedObject *pObject )
{
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) );
COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::kValue );
m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject );
}
// This is a helper class purely to help VC will template type deduction. The real signature we want for the
// base ModifyObject function is:
//
// template < typename TSharedObject >
// void ModifyObject( TSharedObject *pObject, const std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) >& funcModifyAndAudit )
//
// ...but if we do do that, VC will complain that it doesn't know how to deduce the type for TSharedObject
// because of the second parameter. Instead, we make it deduce the type based on the first parameter, and then
// feed that type into this helper class to get the type for the second parameter.
template < typename TSharedObject >
struct CSharedObjectModifyAndAuditFunction
{
typedef std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) > ModifyFunctionType;
};
// ModifyObject: takes in
template < typename TSharedObject >
void ModifyObject( TSharedObject *pObject, const typename CSharedObjectModifyAndAuditFunction<TSharedObject>::ModifyFunctionType& funcModifyAndAudit )
{
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) );
CSharedObject *pWritableObject = NULL;
m_bTransactionBuildSuccess &= BTrackModifiedObjectInternal( pObject, &pWritableObject );
AssertMsg( !m_bTransactionBuildSuccess || pWritableObject != NULL, "Cannot be tracking state for an object but not having a writable version!" );
m_bTransactionBuildSuccess &= funcModifyAndAudit( *m_pSQLAccess, m_SODirtyList, assert_cast<TSharedObject *>( pWritableObject ) );
}
// ModifyObjectL() is a helper macro designed to behave like a function. It takes as parameters the original
// object you want to modify and the code you want to run on it, expressed as a lambda.
#define ModifyObjectL( obj_, modifyfunc_ ) \
ModifyObject( obj_, [=] ( CSQLAccess& sqlAccess, CSharedObjectDirtyList& SODirtyList, decltype( obj_ ) pWritableObject ) -> bool { modifyfunc_ } )
// ModifyObjectSch() is a helper macro designed to behave like a function. It takes as parameters the original
// object you want to modify, an identifier for the field you want to change, and the new value you want to
// change it to. Ex.:
//
// transaction.ModifyObjectSch( pLockedSOCache->GetGameAccount(), // type of internal object is used to look up field IDs
// unNextHalloweenGiftTime, // turns into "(obj).m_unNextHalloweenGiftTime" and "(type)::k_iField_unNextHalloweenGiftTime"
// CRTime::RTime32DateAdd( CRTime::RTime32TimeCur(), tf_halloween_min_minutes_between_drops_per_player.GetInt(), k_ETimeUnitMinute ) );
#define ModifyObjectSch( obj_, field_, newvalue_ ) \
ModifyObjectL( obj_, \
{ \
pWritableObject->Obj().m_ ## field_ = (newvalue_); \
SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k_iField_ ## field_ ); \
return true; \
} )
// ModifyObjectProto() is a helper macro designed to behave like a function. It takes as parameters the original
// object you want to modify, the field name in the message or the field you want to change, the identifier field
// ID (will be identical except for case/underscores), and the new value. Ex.:
//
// soTrans.ModifyObjectProto( pGameAccountClient, // type of internal object is used to look up field IDs
// preview_item_def, // turns into "(obj)->set_preview_item_def"
// PreviewItemDef, // turns into "(type)::kPreviewItemDefFieldNumber"
// 0 );
#define ModifyObjectProto( obj_, fieldfunc_, fieldname_, newvalue_ ) \
ModifyObjectL( obj_, \
{ \
pWritableObject->Obj().set_ ## fieldfunc_( newvalue_ ); \
SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k ## fieldname_ ## FieldNumber ); \
return true; \
} )
// RemoveObject
template < typename TSharedObject >
void RemoveObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit )
{
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) );
COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::kValue );
m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject );
m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject );
}
template < typename TSharedObject >
void RemoveObject( CSharedObject *pObject )
{
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) );
COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::kValue );
m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject );
}
template < class TSchType >
void AddSQLRecord( const TSchType& sch )
{
// We can't test for all types here because there's no compile-time list. This is
// mostly to demonstrate "seriously please don't call this with types that have
// CSharedObject wrappers".
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItem>::kValue) );
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) );
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::kValue) );
// We're about to call a function with "Yielding" in the name and we aren't in a
// function with "Yielding" in the name, but we *are* in a transaction so we know
// we don't yield. We verify that assumption here.
DO_NOT_YIELD_THIS_SCOPE();
// Queue up the work for SQL as long as we're in a good state to do so.
m_bTransactionBuildSuccess &= BIsValidInternalState()
? m_pSQLAccess->BYieldingInsertRecord( &sch )
: false;
}
template < class TSchType >
void AddOrUpdateSQLRecord( TSchType& sch )
{
// We can't test for all types here because there's no compile-time list. This is
// mostly to demonstrate "seriously please don't call this with types that have
// CSharedObject wrappers".
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItem>::kValue) );
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) );
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::kValue) );
// We're about to call a function with "Yielding" in the name and we aren't in a
// function with "Yielding" in the name, but we *are* in a transaction so we know
// we don't yield. We verify that assumption here.
DO_NOT_YIELD_THIS_SCOPE();
// Queue up the work for SQL as long as we're in a good state to do so.
m_bTransactionBuildSuccess &= BIsValidInternalState()
? m_pSQLAccess->BYieldingInsertOrUpdateOnPK( &sch )
: false;
}
// This is meant purely for interop with the SQL message queue and even then only as a temporary
// measure until we have a CSQLTransaction object we can pass around instead.
CSQLAccess& GetSQLTransactionForSQLMsgQueue() { return *m_pSQLAccess; }
// slow! but fine for current uses
template < class TSharedObject >
const TSharedObject *FindTypedSharedObject( const CSharedObject &soIndex ) const
{
return assert_cast<TSharedObject *>( InternalFindSharedObject( pSOCache, soIndex ) );
}
// Take all the work we queued up for SQL and try to commit it to the DB. If that works, take all
// of our memory changes and copy them over. From the outside, this will either move *all* memory
// changes to our cache over at once, or not touch any in-memory structures at all.
MUST_CHECK_RETURN bool BYieldingCommit();
// Cancel this transaction completely -- this will empty the queue of whatever SQL work we may have
// done and also free up the memory we used to track modified object state, etc. Once this function
// gets called, it's illegal to call any other functions on this transaction object except the
// destructor.
void Rollback();
private:
// State validation. Non-const because may set internal error state.
bool BIsValidInternalState();
bool BIsValidInput( const CSharedObject *pObject );
const char *GetInternalTransactionDesc() const { return m_pSQLAccess->PchTransactionName(); }
template < typename tCommitInfo >
const tCommitInfo *InternalFindCommitInfo( const CSharedObject *pObject, const CUtlVector<tCommitInfo>& vec ) const
{
FOR_EACH_VEC( vec, i )
{
if ( vec[i].m_pObject == pObject )
return &vec[i];
}
return NULL;
}
const CSharedObject *InternalFindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject& soIndex ) const;
// Will return NULL if pre-commit operations/checks were successful, or a pointer to a descriptive error string if
// pre-commit failed.
const char *InternalPreCommit();
bool BAddNewObjectInternal( CSharedObject *pObject );
bool BTrackModifiedObjectInternal( CSharedObject *pObject, CSharedObject **out_ppWritableObject );
bool BRemoveObjectInternal( CSharedObject *pObject );
friend class CTrustedHelper_OutputAndSetErrorState;
void SetErrorState() { m_bTransactionBuildSuccess = false; }
private:
CGCSharedObjectCache *m_pLockedSOCache; // we don't do any locking ourself, but verify that the lock is held during construction/modification
CSharedObjectDirtyList m_SODirtyList;
CSQLAccess *m_pSQLAccess; // our access to SQL -- may point to our inline instance or may point to an external object if we're hitching on an already-existing transaction
bool m_bTransactionBuildSuccess;
CSQLAccess m_sqlAccessInternal;
struct CreateOrDestroyCommitInfo_t
{
CreateOrDestroyCommitInfo_t( CSharedObject *pObject ) : m_pObject( pObject ) { Assert( m_pObject ); }
CSharedObject *m_pObject;
};
struct ModifyCommitInfo_t
{
ModifyCommitInfo_t( CSharedObject *pWriteableObject, CSharedObject *pOriginalCopy )
: m_pWriteableObject( pWriteableObject )
, m_pObject( pOriginalCopy )
{
}
CSharedObject *m_pWriteableObject; // scratch/memory-writable copy while transaction is open
CSharedObject *m_pObject; // original copy
};
CUtlVector<CreateOrDestroyCommitInfo_t> m_vecObjects_Added;
CUtlVector<CreateOrDestroyCommitInfo_t> m_vecObjects_Removed;
CUtlVector<ModifyCommitInfo_t> m_vecObjects_Modified;
};
class CSharedObjectTransaction
{
public:
/**
* Constructor that will begin a transaction
* @param sqlAccess
* @param pName
*/
CSharedObjectTransaction( CSQLAccess &sqlAccess, const char *pName );
/**
* Destructor
*/
~CSharedObjectTransaction();
/**
* Adds an object that exists in the given CGCSharedObjectCache to be managed in this transaction.
* Call this before making any modifications to the object
* @param pSOCache the owner CGCSharedObjectCache
* @param pObject the object that will be modified
*/
void AddManagedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject );
/**
* Adds a brand new object to the given CGCSharedObjectCache
* @param pSOCache the owner CGCSharedObjectCache
* @param pObject the newly created object
*/
void AddNewObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject );
/**
* Removes an existing object from the CGCSharedObjectCache
* @param pSOCache the owner CGCSharedObjectCache
* @param pObject the object to be removed from the CGCSharedObjectCache
*/
void RemoveObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject );
/**
* Marks in the transaction that the object was modified. The object must have been previously added via
* the AddManagedObject() call in order for the object to be marked dirty. If the object is new to the
* CGCSharedObjectCache, then calling this will return false (which is not necessarily an error)
*
* @param pObject the object that will be modified
* @param unFieldIdx the field that was changed
*/
void ModifiedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject, uint32 unFieldIdx );
/**
* @param pSOCache
* @param soIndex
* @return the CSharedObject that matches either in the CGCSharedObjectCache or to be added
*/
template < class T >
T *FindTypedSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex )
{
return assert_cast<T *>( FindSharedObject( pSOCache, soIndex ) );
}
/**
* Rolls back any changes made to the objects in-memory and in the database
*
* This function should not be made virtual -- it's called from within the destructor.
*/
void Rollback();
/**
* Commits any changes to the database and also to memory
* @return true if successful, false otherwise
*/
bool BYieldingCommit( bool bAllowEmpty = false );
/**
* @return GCSDK::CSQLAccess associated with this transaction
*/
CSQLAccess &GetSQLAccess() { return m_sqlAccess; }
/**
* Fetch name of transaction for debugging purposes
* @return the string passed to the constructor
*/
const char *PchName() const;
private:
/**
* @param pSOCache
* @param soIndex
* @return the CSharedObject that matches either in the list of currently-modified objects or the list
* or of new objects we added; this will not search in the base SO cache
*/
CSharedObject *FindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex );
/**
* Reverts all in-memory modifications and deletes all newly created objects.
*
* This function should not be made virtual -- it's called from within the destructor.
*/
void Undo();
/**
* Set an error string to describe an error we encountered building this transaction. Setting this
* will cause the transaction to fail.
*/
void SetError( const char *pszNewError )
{
AssertMsg( pszNewError && ( pszNewError[0] != '\0' ), "Invalid NULL/empty error set in CSharedObjectTransaction::SetError()! This will have the effect of clearing the error state, which is unsupported." );
m_sErrorDesc = pszNewError;
}
/**
* Clear our error string if we have one. This will allow transactions to succeed and so is only intended
* to be done when the transaction itself has either completed (no either to begin with) or emptied (clean
* slate).
*/
void ClearError()
{
Assert( m_vecObjects_Added.Count() == 0 );
Assert( m_vecObjects_Removed.Count() == 0 );
Assert( m_vecObjects_Modified.Count() == 0 );
m_sErrorDesc.Clear();
}
/**
* Get access to the string describing what, if any, the last error we encountered was. Will return
* NULL if no errors have been encountered.
*/
const char *GetError() const
{
return m_sErrorDesc.IsEmpty() ? NULL : m_sErrorDesc.String();
}
struct undoinfo_t
{
CSharedObject *pObject;
CGCSharedObjectCache *pSOCache;
CSharedObject *pOriginalCopy;
bool operator==( const undoinfo_t& other ) const { return other.pObject == pObject && other.pSOCache == pSOCache && other.pOriginalCopy == pOriginalCopy; }
};
// Wraps the common check to make sure these pointers are valid and that the cache is locked. This is non-const
// because it can call SetError().
bool AssertValidInput( const CGCSharedObjectCache *pSOCache, const CSharedObject *pObject, const char *pszContext );
// Finds the object in the given vector (using simple pointer compare)
undoinfo_t *FindObjectInVector( const CSharedObject *pObject, CUtlVector<undoinfo_t> &vec ) const;
// variables
CUtlVector< undoinfo_t > m_vecObjects_Added;
CUtlVector< undoinfo_t > m_vecObjects_Removed;
CUtlVector< undoinfo_t > m_vecObjects_Modified;
CSQLAccess &m_sqlAccess;
// internal error state
CUtlString m_sErrorDesc; // will be non-empty if we've encountered an error at some point building this transaction
}; // class CSharedObjectTransaction
}; // namespace GCSDK
#endif // SHAREDOBJECTTRANSACTION_H
|