diff --git a/core/apas/apa_unspentdecay_current.class.php b/core/apas/apa_unspentdecay_current.class.php
new file mode 100644
index 000000000..ed2458617
--- /dev/null
+++ b/core/apas/apa_unspentdecay_current.class.php
@@ -0,0 +1,190 @@
+.
+ */
+
+if ( !defined('EQDKP_INC') ){
+ die('Do not access this file directly.');
+}
+
+if ( !class_exists( "apa_unspentdecay_current" ) ) {
+ class apa_unspentdecay_current extends apa_type_generic {
+ public static $shortcuts = array('apa' => 'auto_point_adjustments');
+
+ protected $ext_options = array(
+ 'decay_time' => array(
+ 'type' => 'spinner',
+ 'max' => 99,
+ 'min' => 1,
+ 'step' => 0.5,
+ 'size' => 2,
+ 'default' => 1
+ ),
+ 'start_date' => array(
+ 'type' => 'datepicker',
+ 'timepicker' => true,
+ 'default' => 'now',
+ ),
+ 'event' => array(
+ 'type' => 'dropdown',
+ 'options' => array(),
+ ),
+ );
+
+ private $modules_affected = array('current_unspentdecay');
+
+ private $cached_data = array();
+
+ public function __construct() {
+ $this->ext_options['start_date']['value'] = $this->time->time;
+ $events = $this->pdh->aget('event', 'name', 0, array($this->pdh->get('event', 'id_list')));
+ if(!empty($events)) {
+ foreach($events as $id => $name) {
+ $this->ext_options['event']['options'][$id] = $name;
+ }
+ }
+ $this->options = array_merge($this->options, $this->ext_options);
+ }
+
+ public function update_point_cap($apa_id) {
+
+ }
+
+ public function modules_affected($apa_id) {
+ return $this->modules_affected;
+ }
+
+ public function get_last_run($date, $apa_id) { return; }
+ public function get_next_run($apa_id) { return 0; }
+
+ public function get_value($apa_id, $cache_date, $module, $dkp_id, $data, $refdate, $debug=false) {
+ $adjustment_event_id = $this->apa->get_data('event', $apa_id);
+ // Fetch decay time (in days) and convert to seconds.
+ $decay_time = $this->apa->get_data('decay_time', $apa_id) * 24*60*60;
+
+ $member_id = $data['member_id'];
+ $multidkp_id = $data['multidkp_id'];
+ $event_id = $data['event_id'];
+ $itempool_id = $data['itempool_id'];
+ $with_twink = $data['with_twink'];
+
+ // The unspent decay works as follows:
+ // We decay any positive earnings after a decay period if these earnings have not been spent.
+ // The spending of points works in a first in, first out basis, i.e., the earliest earnings are spent first.
+ // The main assumption is that the given event ID is *only* used for these automatic adjustments.
+ // We define "earnings" as the points earned in raids *and* positive adjustments.
+ // We need to decay all such "earnings" after the `decay_time` and can check what the last decayed
+ // earning was by looking at the most recent adjustment for our event ID.
+ // The algorithm to calculate what amount to decay is fairly simple. Note, however, that it needs
+ // to group all "earnings" that happened at the same time together.
+ // Let's say we have a set of "earnings" at time t1, which will decay at time t2.
+ // We can calculate whether all of these earnings have been spent at time t2 by getting the point balance
+ // at time t2 and checking whether it is larger than all of the earnings from t1+1 to t2.
+ // If it is larger, we need to adjust the difference as these points decayed.
+
+ // Prevent the case, that main+twinks(=> with_twink=true) is adjusted,
+ // if twinks are shown (so main and twink get own points).
+ if(!($with_twink != $this->config->get('show_twinks'))){
+ return array($data['val'], false, 0);
+ }
+
+ // Get the most recent decay adjustment made.
+ $last_adjustment_id = $this->pdh->get('adjustment', 'most_recent_adj_of_event_member', array($adjustment_event_id, $member_id, $with_twink));
+ if($last_adjustment_id !== false) {
+ // Fetch last adjustment date if there is any.
+ $last_adjustment_date = $this->pdh->get('adjustment', 'date', array($last_adjustment_id));
+ // From here, calculate the last adjusted earnings date (adjustment date - decay time).
+ $last_decayed_earning_date = $last_adjustment_date - $decay_time;
+ } else {
+ // Else, we don't have any adjustments yet and want to start with the start date set.
+ $last_decayed_earning_date = $this->apa->get_data('start_date', $apa_id);
+ }
+
+ // Retrieve all decayed earnings to be processed (we only need dates):
+ // $last_decayed_earning_date < earning date <= current time - $decay_time
+ $earning_dates = array();
+ // Add all raids.
+ $raids = $this->pdh->get('raid', 'raids_of_member_in_interval', array($member_id, $last_decayed_earning_date + 1, $this->time->time - $decay_time, $with_twink));
+ foreach($raids as $raid_id) {
+ $earning_date = $this->pdh->get('raid', 'date', array($raid_id));
+ $earning_value = $this->pdh->get('raid', 'value', array($raid_id));
+ if(!array_key_exists($earning_date, $earning_dates)) {
+ $earning_dates[$earning_date] = 0;
+ }
+ $earning_dates[$earning_date] += $earning_value;
+ }
+
+ // We need to add positive adjustments in that period.
+ $adjustments = $this->pdh->get('adjustment', 'adj_of_member_in_interval', array($member_id, $last_decayed_earning_date + 1, $this->time->time - $decay_time, $with_twink));
+ foreach($adjustments as $adj_id) {
+ $adj_value = $this->pdh->get('adjustment', 'value', array($adj_id));
+ if($adj_value > 0) {
+ $earning_date = $this->pdh->get('adjustment', 'date', array($adj_id));
+ $earning_value = $this->pdh->get('adjustment', 'value', array($adj_id));
+ if(!array_key_exists($earning_date, $earning_dates)) {
+ $earning_dates[$earning_date] = 0;
+ }
+ $earning_dates[$earning_date] += $earning_value;
+ }
+ }
+
+ // Get unique, sorted dates of earnings to decay.
+ ksort($earning_dates);
+
+ // Process them in order.
+ $adjustments_sum = 0;
+ foreach($earning_dates as $earning_date => $earning_value) {
+ // Calculate balance at the end of the decay.
+ $decay_date = $earning_date + $decay_time;
+ $end_balance = $this->pdh->get('points', 'current_history', array($member_id, $multidkp_id, 0, $decay_date, $event_id, $itempool_id, $with_twink, false));
+
+ // Calculate non-decayed earnings during the time period $earning_date+1 to $decay_date.
+ $non_decayed_earnings = 0;
+ $raids = $this->pdh->get('raid', 'raids_of_member_in_interval', array($member_id, $earning_date + 1, $decay_date, $with_twink));
+ foreach($raids as $raid_id) {
+ $raid_value = $this->pdh->get('raid', 'value', array($raid_id));
+ $non_decayed_earnings += $raid_value;
+ }
+ // Add only positive adjustments here.
+ $adjustments = $this->pdh->get('adjustment', 'adj_of_member_in_interval', array($member_id, $earning_date + 1, $decay_date, $with_twink));
+ foreach($adjustments as $adj_id) {
+ $adj_value = $this->pdh->get('adjustment', 'value', array($adj_id));
+ if($adj_value > 0) {
+ $non_decayed_earnings += $adj_value;
+ }
+ }
+
+ // If the balance is greater than the non-decayed earnings, we need to adjust the points.
+ if($end_balance > $non_decayed_earnings) {
+ $adjustment_value = -min($end_balance - $non_decayed_earnings, $earning_value);
+ $adjustments_sum += $adjustment_value;
+ $this->pdh->put('adjustment', 'add_adjustment', array($adjustment_value, $this->apa->get_data('name', $apa_id) . ' for ' . $this->time->user_date($earning_date), $member_id, $adjustment_event_id, NULL, $decay_date));
+ $this->pdh->process_hook_queue();
+ }
+ }
+
+ // Return updated value.
+ return array($data['val'] + $adjustments_sum, false, 0);
+ }
+
+ public function recalculate($apa_id){
+ return true;
+ }
+ }//end class
+}//end if
diff --git a/core/auto_point_adjustments.class.php b/core/auto_point_adjustments.class.php
index 89f2d11a9..9c97cf3df 100644
--- a/core/auto_point_adjustments.class.php
+++ b/core/auto_point_adjustments.class.php
@@ -36,7 +36,8 @@ class auto_point_adjustments extends gen_class {
private $decayed_pools = array();
private $cap_pools = array();
private $hardcap_pools = array();
- private $currentcap_pools = array();
+ private $currentcap_pools = array();
+ private $unspentdecay_pools = array();
private $apa_types_inst = array();
@@ -205,7 +206,8 @@ public function is_decay($module, $pool) {
if(empty($this->apa_tab)) return false;
if(empty($this->decayed_pools)) {
foreach($this->apa_tab as $apa_id=> $apa) {
- if(stripos($apa['type'], 'decay') === false) continue;
+ // Exclude unspentdecay here.
+ if(stripos($apa['type'], 'decay') === false || stripos($apa['type'], 'unspentdecay') !== false) continue;
$modules = $this->get_apa_type($apa['type'])->modules_affected($apa_id);
foreach($apa['pools'] as $dkp_id) {
foreach($modules as $_module) {
@@ -271,6 +273,24 @@ public function is_currentcap($module, $pool) {
return false;
}
+ public function is_unspentdecay($module, $pool) {
+ if(empty($this->apa_tab)) return false;
+ if(empty($this->unspentdecay_pools)) {
+ foreach($this->apa_tab as $apa_id=> $apa) {
+
+ if(stripos($apa['type'], 'unspentdecay') === false) continue;
+ $modules = $this->get_apa_type($apa['type'])->modules_affected($apa_id);
+ foreach($apa['pools'] as $dkp_id) {
+ foreach($modules as $_module) {
+ $this->unspentdecay_pools[$dkp_id][] = $_module;
+ }
+ }
+ }
+ }
+ if(!empty($this->unspentdecay_pools[$pool]) && in_array($module, $this->unspentdecay_pools[$pool])) return true;
+ return false;
+ }
+
public function get_caption($module, $pool) {
foreach($this->apa_tab as $apa) {
foreach($apa['pools'] as $dkp_id) {
diff --git a/core/data_handler/includes/modules/read/adjustment/pdh_r_adjustment.class.php b/core/data_handler/includes/modules/read/adjustment/pdh_r_adjustment.class.php
index 08b293184..00ea006b3 100644
--- a/core/data_handler/includes/modules/read/adjustment/pdh_r_adjustment.class.php
+++ b/core/data_handler/includes/modules/read/adjustment/pdh_r_adjustment.class.php
@@ -200,6 +200,67 @@ public function get_adjsofeventid($event_id) {
return $this->objPagination->search("event_id", $event_id);
}
+ /**
+ * Returns the most recent (date wise) adjustment made for a given event and member.
+ * @param integer $event_id
+ * @param integer $member_id
+ * @param boolean $with_twink
+ * @return integer/boolean adjustment object id
+ */
+ public function get_most_recent_adj_of_event_member($event_id, $member_id, $with_twink=true) {
+ $member_ids = array($member_id);
+ if($with_twink) {
+ if(!$this->pdh->get('member', 'is_main', array($member_id))) {
+ $member_id = $this->pdh->get('member', 'mainid', array($member_id));
+ }
+
+ $twinks = $this->pdh->get('member', 'other_members', $member_id);
+ $member_ids = array_merge($member_ids, $twinks);
+ }
+
+ $objQuery = $this->db->prepare("SELECT adjustment_id FROM __adjustments WHERE member_id :in AND event_id = ? ORDER BY adjustment_date DESC LIMIT 1")->in($member_ids)->execute($event_id);
+
+ if($objQuery){
+ if($row = $objQuery->fetchAssoc()){
+ return (int)$row['adjustment_id'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the adjustments made for a given member within a time period.
+ * @param integer $member_id
+ * @param integer $from
+ * @param integer $to
+ * @param boolean $with_twink
+ * @return array adjustment object ids
+ */
+ public function get_adj_of_member_in_interval($member_id, $from=0, $to=PHP_INT_MAX, $with_twink=true) {
+ $member_ids = array($member_id);
+ if($with_twink) {
+ if(!$this->pdh->get('member', 'is_main', array($member_id))) {
+ $member_id = $this->pdh->get('member', 'mainid', array($member_id));
+ }
+
+ $twinks = $this->pdh->get('member', 'other_members', $member_id);
+ $member_ids = array_merge($member_ids, $twinks);
+ }
+
+ $objQuery = $this->db->prepare("SELECT adjustment_id FROM __adjustments WHERE member_id :in AND adjustment_date >= ? AND adjustment_date <= ?")->in($member_ids)->execute($from, $to);
+
+ $adjustment_ids = array();
+
+ if($objQuery){
+ while($row = $objQuery->fetchAssoc()){
+ $adjustment_ids[] = (int)$row['adjustment_id'];
+ }
+ }
+
+ return $adjustment_ids;
+ }
+
public function get_group_key($adj_id){
return $this->objPagination->get($adj_id, "adjustment_group_key");
}
diff --git a/core/data_handler/includes/modules/read/points/pdh_r_points.class.php b/core/data_handler/includes/modules/read/points/pdh_r_points.class.php
index 9ec73e087..d4f9a0f71 100644
--- a/core/data_handler/includes/modules/read/points/pdh_r_points.class.php
+++ b/core/data_handler/includes/modules/read/points/pdh_r_points.class.php
@@ -36,6 +36,7 @@ class pdh_r_points extends pdh_r_generic{
private $hardcap = array();
private $currentcap = array();
private $riadecayed = array();
+ private $unspentdecayed = array();
private $arrCalculatedSingle = array();
private $arrCalculatedMulti = array();
private $arrSnapshotTime = array();
@@ -397,6 +398,7 @@ public function get_current($member_id, $multidkp_id, $event_id=0, $itempool_id=
if(!isset($this->decayed[$multidkp_id])) $this->decayed[$multidkp_id] = $this->apa->is_decay('current', $multidkp_id);
if(!isset($this->hardcap[$multidkp_id])) $this->hardcap[$multidkp_id] = $this->apa->is_hardcap('current_hardcap', $multidkp_id);
if(!isset($this->currentcap[$multidkp_id])) $this->currentcap[$multidkp_id] = $this->apa->is_currentcap('current_currentcap', $multidkp_id);
+ if(!isset($this->unspentdecayed[$multidkp_id])) $this->unspentdecayed[$multidkp_id] = $this->apa->is_unspentdecay('current_unspentdecay', $multidkp_id);
if($with_apa && $this->decayed[$multidkp_id]) {
$data = array(
@@ -413,6 +415,19 @@ public function get_current($member_id, $multidkp_id, $event_id=0, $itempool_id=
} else {
$value = ($this->get_earned($member_id, $multidkp_id, $event_id, $with_twink) - $this->get_spent($member_id, $multidkp_id, $event_id, $itempool_id, $with_twink) + $this->get_adjustment($member_id, $multidkp_id, $event_id, $with_twink));
}
+
+ if($with_apa && $this->unspentdecayed[$multidkp_id]){
+ $data = array(
+ 'id' => $multidkp_id.'_'.$member_id.'_'.(($with_twink) ? 1 : 0),
+ 'val' => $value,
+ 'member_id' => $member_id,
+ 'multidkp_id' => $multidkp_id,
+ 'event_id' => $event_id,
+ 'itempool_id' => $itempool_id,
+ 'with_twink' => ($with_twink) ? true : false,
+ );
+ $value = $this->apa->get_value('current_unspentdecay', $multidkp_id, $this->time->time, $data);
+ }
if($with_apa && $this->currentcap[$multidkp_id]){
$data = array(
diff --git a/core/data_handler/includes/modules/read/raid/pdh_r_raid.class.php b/core/data_handler/includes/modules/read/raid/pdh_r_raid.class.php
index 6ce1e4fc3..adc1e293f 100644
--- a/core/data_handler/includes/modules/read/raid/pdh_r_raid.class.php
+++ b/core/data_handler/includes/modules/read/raid/pdh_r_raid.class.php
@@ -282,6 +282,38 @@ public function get_raididsindateinterval($start_date, $end_date, $event_ids=fal
return $arrRaids;
}
+ /**
+ * Returns the raids a given member attended within a time period.
+ * @param integer $member_id
+ * @param integer $from
+ * @param integer $to
+ * @param boolean $with_twink
+ * @return array raid object ids
+ */
+ public function get_raids_of_member_in_interval($member_id, $from=0, $to=PHP_INT_MAX, $with_twink=true) {
+ $member_ids = array($member_id);
+ if($with_twink) {
+ if(!$this->pdh->get('member', 'is_main', array($member_id))) {
+ $member_id = $this->pdh->get('member', 'mainid', array($member_id));
+ }
+
+ $twinks = $this->pdh->get('member', 'other_members', $member_id);
+ $member_ids = array_merge($member_ids, $twinks);
+ }
+
+ $objQuery = $this->db->prepare("SELECT r.raid_id AS raid_id FROM __raids AS r JOIN __raid_attendees AS ra ON r.raid_id = ra.raid_id WHERE ra.member_id :in AND r.raid_date >= ? AND r.raid_date <= ?")->in($member_ids)->execute($from, $to);
+
+ $adjustment_ids = array();
+
+ if($objQuery){
+ while($row = $objQuery->fetchAssoc()){
+ $adjustment_ids[] = (int)$row['raid_id'];
+ }
+ }
+
+ return $adjustment_ids;
+ }
+
public function get_lastnraids($count = 1){
$objQuery = $this->db->prepare("SELECT raid_id FROM __raids ORDER BY raid_date DESC")->limit($count)->execute();
diff --git a/language/english/lang_admin.php b/language/english/lang_admin.php
index 909865120..50913e7ee 100644
--- a/language/english/lang_admin.php
+++ b/language/english/lang_admin.php
@@ -534,7 +534,7 @@
"pdc_memcache_server_help" => 'Address of the Memcache Server (e.g. 127.0.0.1 for localhost)',
"pdc_memcache_port_text" => 'Memcache Server-Port',
"pdc_memcache_port_help" => 'Port where the Memcache Server can be contacted (default: 11211)',
- "pdc_redis_server_text" => 'Redis Server Address',
+ "pdc_redis_server_text" => 'Redis Server Address',
"pdc_redis_server_help" => 'Address of the Redis Server (e.g. 127.0.0.1 for localhost)',
"pdc_redis_port_text" => 'Redis Server-Port',
"pdc_redis_port_help" => 'Port where the Redis Server can be contacted (default: 6379) or Socket',
@@ -543,8 +543,8 @@
"pdc_cache_info_xcache" => 'XCache. View this for more details.',
"pdc_cache_info_apc" => 'Alternative PHP Cache (APC). View this for more details.',
"pdc_cache_info_memcache" => 'Memcache. View this for more details.',
- "pdc_cache_info_memcached" => "Memcached. View hier for more details.",
- "pdc_cache_info_redis" => "Redis In-Memory Cache. View hier for more details.",
+ "pdc_cache_info_memcached" => "Memcached. View hier for more details.",
+ "pdc_cache_info_redis" => "Redis In-Memory Cache. View hier for more details.",
"pdc_expire_time" => 'Date of expiry',
"pluskernel" => 'PLUS Config',
"attention" => 'Warning',
@@ -824,6 +824,7 @@
"apa_of_type" => 'of type',
"apa_start_date" => 'Start date',
"apa_decay_ria_start_date_help" => 'Only raids/items/adjustments after this date will be considered.',
+ "apa_unspentdecay_current_start_date_help" => 'Only earnings after this date will be considered.',
"apa_exectime" => 'time of execution',
"apa_pools" => 'Point accounts',
"apa_zero_time" => 'Period, after the points become 0.',
@@ -846,11 +847,15 @@
"apa_cap_current_interval" => "Check point cap every x days",
"apa_cap_current_event" => "Event for the adjustment",
"apa_cap_current_twinks" => "Check twinks separately?",
+ "apa_unspentdecay_current_decay_time_help" => "Remove unspent points x days after earning them (points are spent on the first non-decayed points earned).",
+ "apa_unspentdecay_current_event" => "Event for the adjustment",
+ "apa_unspentdecay_current_event_help" => "Make sure this is a separate event that is only used by this decay mechanism.",
"apa_type_decay_ria" => 'Decay on Raids, Items and Adjustments',
"apa_type_decay_current" => 'Decay on Current',
"apa_type_cap_current" => 'Maximum points (repeating)',
"apa_type_hardcap_current" => "Maximum points (visible)",
"apa_type_currentcap_current" => "Maximum points (hard)",
+ "apa_type_unspentdecay_current" => "Decay unspent",
"apa_type_startpoints" => 'Start points',
"apa_type_onetime_current" => "Onetime adjustment",
"apa_type_inactivity" => 'Inactivity',
diff --git a/language/german/lang_admin.php b/language/german/lang_admin.php
index 344671b6b..255a4c730 100644
--- a/language/german/lang_admin.php
+++ b/language/german/lang_admin.php
@@ -821,6 +821,7 @@
"apa_of_type" => "vom Typ",
"apa_start_date" => "Startdatum",
"apa_decay_ria_start_date_help" => "Nur Raids/Items/Korrekturen nach diesem Datum werden berücksichtigt.",
+"apa_unspentdecay_current_start_date_help" => "Nur Punkte ab diesem Datum können verfallen.",
"apa_exectime" => "Ausführzeit (HH:MM)",
"apa_pools" => "Punktekonten",
"apa_zero_time" => "Zeitraum, nach dem Punkte 0 werden.",
@@ -849,6 +850,10 @@
"apa_hardcap_current_lower_cap" => "Unteres Punktelimit",
"apa_onetime_current_event" => "Ereignis für die Korrektur",
"apa_onetime_current_twinks" => "Twinks separat behandeln?",
+"apa_type_unspentdecay_current" => "Verfall ungenutzter Punkte",
+"apa_unspentdecay_current_decay_time_help" => "Entferne nicht verbrauchte Punkte x Tage nachdem sie gutgeschrieben wurden (Punkte werden mit frühsten, nicht verfallenen Gutschriften verrechnet).",
+"apa_unspentdecay_current_event" => "Ereignis für die Korrektur",
+"apa_unspentdecay_current_event_help" => "Stelle sicher, dass dies ein separates Event ist, welches nur für den Verfallsmechanismus verwendet wird.",
"apa_type_startpoints" => "Startpunkte",
"apa_type_inactivity" => "Inaktivität",
"apa_type_activity" => "Aktivität",